当前位置: 首页 > news >正文

C++初阶(七)--类和对象(4)

目录

​编辑

一、再谈构造函数

1.构造函数体赋值

2.初始化列表

二、类型转换 

1.隐式类型转换

2.explicit关键字

3.类类型之间的对象隐式转换

三、static成员函数

1.概念

2.特性

3.面试题:

四、友元函数

1.基本介绍

2.回顾:

3.友元类:

4.作用:

0x01. 提供灵活的访问方式

0x02. 实现不同类之间的协作

5.注意事项

五、内部类

1.概念

2.特性

 六、匿名对象

1.定义

 2.匿名对象的生命周期特点

3.匿名对象的适用场景

七、对象拷贝时的优化

题目加餐


一、再谈构造函数

1.构造函数体赋值

 在创建对象时,编译器会通过调用构造函数,给对象中的各个成员变量一个合适的初始值:

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;
}

        需要注意的是:虽然通过调用上述的构造函数后,对象中的每个成员变量都有了一个初始值,但是构造函数中的语句只能将其称作为赋初值,而不能称作为初始化。因为初始化只能初始化一次,而构造函数体内可以进行多次赋值。

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.初始化列表

初始化列表定义:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式。

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.类中包含以下成员,必须放在初始化列表进行初始化:
引用成员变量
 引用类型的变量在定义时就必须给其一个初始值,所以引用成员变量必须使用初始化列表对其进行初始化。

	int a = 10;int& b = a;// 创建时就初始化

const成员变量
 被const修饰的变量也必须在定义时就给其一个初始值,也必须使用初始化列表进行初始化。

	const int a = 10;//correct 创建时就初始化const int b;//error 创建时未初始化

自定义类型成员(该类没有默认构造函数)
 若一个类没有默认构造函数,那么我们在实例化该类对象时就需要传参对其进行初始化,所以实例化没有默认构造函数的类对象时必须使用初始化列表对其进行初始化。
 在这里再声明一下,默认构造函数是指不用传参就可以调用的构造函数:
 1.我们不写,编译器自动生成的构造函数。
 2.无参的构造函数。
 3.全缺省的构造函数。

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.对于内置类型,使用初始化列表和在构造函数体内进行初始化实际上是没有差别的,其差别就类似于如下代码:

// 使用初始化列表
int a = 10
// 在构造函数体内初始化(不使用初始化列表)
int a;
a = 10;

2.对于自定义类型,使用初始化列表可以提高代码的效率

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类的构造函数。
 我们若是想在不使用初始化列表的情况下,达到我们想要的效果,就不得不这样写了:

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.成员变量在类中声明的次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后顺序无关
举个例子:

class A
{
public:A(int a):_a1(a), _a2(_a1){}void Print() {cout << _a1 << " " << _a2 << endl;}
private://根据类声明顺序进行初始化 先初始化_a2 再初始化_a1int _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++ 编译器在连续的一个过程中,编译器为了提高效率,多个构造会被优化,合二为一。

#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等价于以下两句代码: 

Date tmp(2024); //先构造
Date d1(tmp); //再拷贝构造

所以在早期的编译器中,当编译器遇到Date d1 = 2024这句代码时,会先构造一个临时对象,再用临时对象拷贝构造d1;但是现在的编译器已经做了优化,当遇到Date d1 = 2024这句代码时,会按照Date d1(2024)这句代码处理,这就叫做隐式类型转换。

 实际上,我们早就接触了隐式类型转换,只是我们不知道而已,以下代码也叫隐式类型转换:

int a = 10;
double b = a; //隐式类型转换

在这个过程中,编译器会先构建一个double类型的临时变量接收a的值,然后再将该临时变量的值赋值给b。这就是为什么函数可以返回局部变量的值,因为当函数被销毁后,虽然作为返回值的变量也被销毁了,但是隐式类型转换过程中所产生的临时变量并没有被销毁,所以该值仍然存在。

2.explicit关键字

对于单参数的自定义类型来说,Date d1 = 2021这种代码的可读性不是很好,我们若是想禁止单参数构造函数的隐式转换,可以用关键字explicit来修饰构造函数。

#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.类类型之间的对象隐式转换

类类型的对象之间也可以隐式转换,需要相应的构造函数⽀持。

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的对象a1ClassA a1(10);// 这里发生了隐式转换,将ClassA类型的对象a1隐式转换为ClassB类型的对象b1ClassB 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修饰的成员函数,称之为静态成员函数。静态成员变量通常是在类外进行初始化

class Test
{
private:static int _n;
};
// 静态成员变量的定义初始化
int Test::_n = 0;

2.特性

• 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。

#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指针。

class Test
{
public:static void Fun(){cout << _a << endl; //error不能访问非静态成员cout << _n << endl; //correct}
private:int _a; //非静态成员static int _n; //静态成员
};

小贴士:含有静态成员变量的类,一般含有一个静态成员函数,用于访问静态成员变量。 

• ⾮静态的成员函数,可以访问任意的静态成员变量和静态成员函数。

• 突破类域就可以访问静态成员,可以通过类名::静态成员或者对象.静态成员来访问静态成员变量 和静态成员函数。

3.访问静态成员变量的方法:

0x01.当静态成员变量为公有时,有以下几种访问方式:

#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.当静态成员变量为私有时,有以下几种访问方式:

#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.面试题:

为什么静态成员变量不能在声明位置用缺省值初始化(通过构造函数初始化列表的方式)

由于静态成员变量不属于某个具体对象,它的生命周期和整个类相关,在程序运行开始时就已经存在(在类被加载时就进行了相应的内存分配等操作),而不是随着对象的创建才进行初始化。构造函数是在创建对象时被调用的,其初始化列表也是针对对象的成员变量进行初始化操作的。所以静态成员变量不能按照对象的构造函数初始化列表的方式来给定缺省值初始化。

如果要对静态成员变量进行初始化,通常有以下几种正确的做法:

1. 在类外进行初始化

在类的定义之外,在全局作用域或者某个合适的命名空间内,对静态成员变量进行初始化。

class MyClass {
public:static int staticVar;
};// 在类外初始化静态成员变量
int MyClass::staticVar = 10;

2. 使用静态成员函数或者静态代码块进行初始化(在 C++11 及以后版本更灵活的方式)

可以利用静态成员函数或者静态代码块在程序运行时的合适时机对静态成员变量进行初始化。

例如,使用静态成员函数:

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输入输出,是因为库里面已经将它们的<<和>>重载好了,<<和>>能够自动识别类型,是因为它们之间构成了函数重载。

所以,我们若是想让<<和>>也自动识别我们的日期类,就需要我们自己写出对应的运算符重载函数。 

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.友元类:

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

#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. 实现不同类之间的协作

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中非公有成员。

// 前置声明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、枚举成员,不需要外部类的对象/类名。
 • 外部类的大小与内部类的大小无关。

class A
{
private:static int k;int h;
public:class B // B天生就是A的友元{public:void foo(const A& a){cout << k << endl;//OKcout << 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.定义

在编程中,当我们使用类型名直接跟上括号并传入实参(如果需要的话)的方式来创建一个对象时,这样创建出来的对象就叫做匿名对象。

MyClass(5);

例如,在 C++ 中,如果有一个类 MyClass ,它有一个合适的构造函数接受某些参数(假设构造函数接受一个整数参数),那么我们可以这样创建匿名对象:

MyClass myObj(5);

 2.匿名对象的生命周期特点

匿名对象的生命周期非常短暂,它仅仅存在于当前代码行的执行期间。一旦当前这行代码执行完毕,匿名对象就会被销毁。

这是因为匿名对象没有一个明确的名字来让我们在后续的代码中继续引用它,所以编译器会在当前行代码执行完相关操作后,自动清理掉这个匿名对象所占用的资源。

#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),我们可以这样调用它:

myFunction(MyClass(15));

 小贴士:匿名对象在一些特定的编程场景中提供了一种简洁、高效的临时使用对象的方式,但由于其生命周期短暂的特点,在使用时需要清楚地了解其适用场景和限制条件。

七、对象拷贝时的优化

现代编译器会为了尽可能提⾼程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传返 回值的过程中可以省略的拷⻉。 如何优化C++标准并没有严格规定,各个编译器会根据情况⾃⾏处理。当前主流的相对新⼀点的编 译器对于连续⼀个表达式步骤中的连续拷⻉会进⾏合并优化,有些更新更"激进"的编译器还会进⾏ 跨⾏跨表达式的合并优化。

#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的话可能会有不一样的结果哦~

int main()
{A aa2 = f2();cout << endl;
}

 输出: 同上

如果编译器没有优化的情况下,从语法角度来讲。这里会有两次拷贝构造函数的调用:一次是在 f2 函数中创建返回的临时对象时,传值返回都会生成一个临时对象,这是语法规定的哦·。另一次是在 main 函数中初始化 aa2 时。

我们可以在g++环境下看到没有优化的情况

 VS2022环境下:

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类创建了多少个对象?

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次调用构造函数时自增的是同一个变量,每个对象访问到的静态成员变量是同一个。同理,存储累加结果的变量也必须是静态成员变量。

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;


本篇博客到此结束,欢迎各位在评论区留言~

相关文章:

C++初阶(七)--类和对象(4)

目录 ​编辑 一、再谈构造函数 1.构造函数体赋值 2.初始化列表 二、类型转换 1.隐式类型转换 2.explicit关键字 3.类类型之间的对象隐式转换 三、static成员函数 1.概念 2.特性 3.面试题&#xff1a; 四、友元函数 1.基本介绍 2.回顾&#xff1a; 3.友元类&am…...

Python 爬虫的寻宝大冒险:如何捕获 API 数据的宝藏

在这个信息爆炸的数字时代&#xff0c;数据就像是隐藏在网络深处的宝藏&#xff0c;等待着勇敢的探险家去发现。今天&#xff0c;我们要讲述的是如何成为一名 Python 爬虫探险家&#xff0c;装备你的代码工具&#xff0c;深入 API 的迷宫&#xff0c;捕获那些珍贵的数据宝藏。 …...

电力物联网环境下的售电研究

泛在电力物联网打破了传统能源网络的壁垒&#xff0c;形成了能源共享、信息互通、数据开放的能源物联网。泛在电力物联网环境下&#xff0c;可再生能源接入更为容易。更加开放的能源接人、更加丰富的信息获取以及更加智能的电力设备&#xff0c;促进了电力市场的进一步开放。 …...

Oracle视频基础1.1.4练习

1.1.4 dbb,ddabcPMON,SMON,LGWR,CKPT,DBWna5,b4,c2,d3,e1ad,a,c,b,eOracle instance,Oracle databaseSGA,background processcontrol file,data file,online redo file 以下是一篇关于 Oracle 基础习题 1.1.4 的博客&#xff1a; Oracle 基础习题解析&#xff1a;1.1.4 本篇文…...

【水下生物数据集】 水下生物识别 深度学习 目标检测 机器视觉 yolo(含数据集)

一、背景意义 随着全球海洋生态环境的日益变化&#xff0c;水下生物的监测和保护变得愈发重要。水下生物种类繁多&#xff0c;包括螃蟹、鱼类、水母、虾、小鱼和海星等&#xff0c;它们在海洋生态系统中扮演着关键角色。传统的水下生物监测方法通常依赖于人工观察&#xff0c;效…...

【宠物狗狗数据集】 犬类品种识别 宠物狗检测 深度学习 目标检测(含数据集)

一、背景意义 随着人们对宠物狗的喜爱日益增加&#xff0c;犬种的多样性也逐渐受到重视。狗狗不仅是家庭的好伴侣&#xff0c;更在多个领域中发挥着重要作用&#xff0c;如导盲、搜救、疗愈等。因此&#xff0c;准确识别和分类各种犬种显得尤为重要。传统的犬种识别方法往往依赖…...

C语言中的数组并非指针:深入理解数组和指针的区别

前言 在C语言中&#xff0c;数组和指针是两个非常重要的概念&#xff0c;它们在很多方面有着紧密的联系&#xff0c;但也存在显著的区别。尽管数组名有时可以像指针那样使用&#xff0c;但它们本质上并不是一回事。理解这些差异对于编写正确和高效的代码至关重要。本文将深入探…...

Topaz Video AI for Mac 视频无损放大软件安装教程【保姆级,操作简单轻松上手】

Mac分享吧 文章目录 Topaz Video AI for Mac 视频无损放大软件 安装完成&#xff0c;软件打开效果一、Topaz Video AI 视频无损放大软件 Mac电脑版——v5.3.5⚠️注意事项&#xff1a;1️⃣&#xff1a;下载软件2️⃣&#xff1a;安装软件&#xff0c;将安装包从左侧拖入右侧文…...

虚函数和纯虚函数是 C++ 中实现多态性的关键概念

虚函数&#xff08;Virtual Function&#xff09; 定义&#xff1a;虚函数是在基类中使用 virtual 关键字声明的函数&#xff0c;目的是允许派生类重写该函数。用途&#xff1a;通过虚函数&#xff0c;基类指针或引用可以调用派生类中重写的函数&#xff0c;从而实现动态多态性…...

计算机网络IP地址分类,子网掩码,子网划分复习资料

IP 地址的概念 IP 地址是独立于硬件地址的逻辑地址&#xff0c;它是由软件提供的地址。 IP 地址是网络层地址。 IP 编址方案和分类 IP 地址由 32 位二进制数构成&#xff0c;分为前缀(网络地址)和后缀(主机地址) 同一网段中每台计算机的 IP 地址是唯一的网络地址的分配全球…...

LINUX下使用SQLite查看.db数据库文件

目录 1. 安装 SQLite 对于 Debian/Ubuntu 系统&#xff1a; 2.安装完成后操作 打开 SQLite 命令行工具并连接到数据库文件 查看表结构 查询表中的数据 执行其他 SQL 操作 3. 退出 SQLite 命令行工具 4. 使用图形化工具&#xff08;可选&#xff09; 总结 在 Linux 环…...

基于uniapp微信小程序的校园二手书交易系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…...

性能测试中的操作系统参数优化

目录 以下是一些针对性能测试的操作系统优化建议 关闭不必要的服务和程序&#xff1a; 更新系统和驱动程序&#xff1a; 优化电源管理设置&#xff1a; 调整内存配置&#xff1a; 网络配置优化&#xff1a; 磁盘I/O优化&#xff1a; 内核参数调整&#xff1a; 安全软件…...

rabbitmq高级特性(2)TTL、死信/延迟队列、事务与消息分发

目录 1.TTL 1.1.设置消息过期时间 1.2.设置队列过期时间 2.死信队列 2.1.介绍 2.2.演示 3.延迟队列 3.1.模拟实现延迟队列 3.2.延迟队列插件 4.事务与消息分发 4.1.事务 4.2.消息分发 1.TTL 所谓的ttl&#xff0c;就是过期时间。对于rabbitmq&#xff0c;可以设置…...

了解一下,RN中怎么加载 threejs的

在React Native&#xff08;RN&#xff09;中加载和使用Three.js&#xff0c;一个流行的3D图形库&#xff0c;通常需要一些额外的步骤&#xff0c;因为Three.js主要是为Web浏览器设计的&#xff0c;而React Native则使用原生的渲染引擎。不过&#xff0c;有一些方法可以在React…...

笔记整理—linux驱动开发部分(1)驱动梗概

驱动可以分为广义上的和狭义上的驱动。广义上的驱动是用于操作硬件的代码&#xff0c;而狭义上的驱动为基于内核系统之上让硬件去被操作的逻辑方法。 linux体系架构&#xff1a; 1.分层思想 &#xff1a;在OS中间还会有许多层。 : 2.驱动的上面是系统调用&#xff08;API&…...

金融领域中的敏感性分析和期权价值计算相关的操作

代码主要进行了金融领域中的敏感性分析和期权价值计算相关的操作。首先通过一系列方程求解S3和S2的值,然后基于这些值以及给定的参数计算一些中间变量(a1、a2、a3、b1、b2、b3),最后利用多元正态分布函数(mvncdf)和一元正态分布函数(normcdf)计算期权价值C、净现值(NP…...

GraphQL系列 - 第1讲 GraphQL语法入门

目录 一、介绍GraphQL二、GraphQL基本使用方法三、Schema 定义语言 (SDL)3.1 类型定义1&#xff09;对象类型2&#xff09;标量类型3&#xff09;枚举类型4&#xff09;输入类型5&#xff09;列表类型6&#xff09;非空类型7&#xff09;接口类型8&#xff09;联合类型 3.2 查询…...

015:地理信息系统开发平台ArcGIS Engine10.2与ArcGIS SDK for the Microsoft .NET Framework安装教程

摘要&#xff1a;本文详细介绍地理信息系统开发平台ArcGIS Engine10.2与ArcGIS SDK for the Microsoft .NET Framework的安装流程。 一、软件介绍 ArcGIS Engine 10.2是由Esri公司开发的一款强大的GIS&#xff08;地理信息系统&#xff09;开发平台。该软件基于ArcGIS 10.2 fo…...

Android——显式/隐式Intent

概述 在Android中&#xff0c;Intent是各个组件之间信息通信的桥梁&#xff0c;它用于Android各组件的通信。 Intent 的组成部分 一、显式 Intent 第一种方式 Intent intent new Intent(this, ActFinishActivity.class);startActivity(intent);第二种方式 Intent intent …...

Python实战:5分钟搞定睿尔曼机械臂与AGV底盘的Socket通信(附完整代码)

Python实战&#xff1a;5分钟搞定睿尔曼机械臂与AGV底盘的Socket通信&#xff08;附完整代码&#xff09; 在工业自动化领域&#xff0c;复合机器人正逐渐成为提升生产效率的关键设备。这类机器人通常由AGV&#xff08;自动导引运输车&#xff09;底盘和机械臂组成&#xff0c;…...

从 99.8% 到 14.9%:Paperxie AI 降重,让论文 AIGC 焦虑彻底成为过去式

paperxie-免费查重复率aigc检测/开题报告/毕业论文/智能排版/文献综述/AIPPThttps://www.paperxie.cn/weight?type1https://www.paperxie.cn/weight?type1 一、写在前面&#xff1a;被 AIGC 检测支配的论文焦虑&#xff0c;终于有解了 当知网、维普等平台全面升级 AIGC 检测…...

DexGraspNet与多指手抓取算法详解:从理论到工程实现

目录 DexGraspNet与多指手抓取算法详解:从理论到工程实现 第一部分:原理详解 第一章 绪论与灵巧抓取的挑战 1.1 机器人抓取技术演进 1.1.1 从平行夹爪到多指灵巧手 1.1.2 灵巧抓取的独特挑战 1.2 DexGraspNet的研究背景与意义 1.2.1 大规模数据驱动的必要性 1.2.2 D…...

自编码器在异常检测中的实战应用:以金融交易数据为例

自编码器在金融异常检测中的实战指南&#xff1a;从数据清洗到模型部署 金融交易数据中的异常行为检测一直是风险控制的核心环节。传统基于规则的系统难以应对日益复杂的欺诈模式&#xff0c;而自编码器这类无监督学习模型正在改变游戏规则。本文将带您从零构建一个完整的异常检…...

华硕笔记本CPU过热?G-Helper降压调优终极指南帮你降温10℃

华硕笔记本CPU过热&#xff1f;G-Helper降压调优终极指南帮你降温10℃ 【免费下载链接】g-helper Lightweight Armoury Crate alternative for Asus laptops. Control tool for ROG Zephyrus G14, G15, G16, M16, Flow X13, Flow X16, TUF, Strix, Scar and other models 项目…...

Claude Tool Use 怎么用?从零到生产的完整教程(2026)

上周接了个需求&#xff0c;做一个能查天气、查数据库、还能发邮件的 AI 助手。一开始想着用 LangChain 套一层&#xff0c;后来发现 Claude 原生的 Tool Use&#xff08;也叫 Function Calling&#xff09;已经很成熟了&#xff0c;根本不需要额外框架。但官方文档写得有点绕&…...

通达信顶底背离副图指标源码解析与实战应用

1. 通达信顶底背离副图指标入门指南 第一次接触顶底背离指标时&#xff0c;我也被那些复杂的线条和公式搞得一头雾水。后来才发现&#xff0c;这其实是技术分析中最实用的趋势反转信号工具之一。简单来说&#xff0c;顶底背离就是当价格创新高或新低时&#xff0c;指标却没有同…...

【无标题】260329

一切都只是我想多了么看到你的博文看到你的新年快乐现在看到你删库跑路为什么要这样出现又消失。。。本来就虚无缥缈的一点儿联系又消失殆尽如果现在可以见到你我心里有N个为什么想问你只是觉得憋屈可能是我理解能力不足共情能力有限我猜不到你的心思啊你到底是想联系还是不想联…...

Qwen3-ASR-1.7B多说话人分离展示:会议录音自动分角色

Qwen3-ASR-1.7B多说话人分离展示&#xff1a;会议录音自动分角色 会议记录不再需要人工分辨谁说了什么&#xff0c;AI现在能帮你自动区分每个发言人 1. 引言 想象一下这样的场景&#xff1a;一场两小时的多人会议刚刚结束&#xff0c;你需要整理会议纪要。传统的做法是反复听录…...

英雄联盟段位修改完整解决方案:LeaguePrank免费工具终极指南

英雄联盟段位修改完整解决方案&#xff1a;LeaguePrank免费工具终极指南 【免费下载链接】LeaguePrank 项目地址: https://gitcode.com/gh_mirrors/le/LeaguePrank 还在为单调的游戏段位显示感到乏味吗&#xff1f;LeaguePrank这款创新的免费工具将彻底改变你的英雄联盟…...