C++——类和对象[下]

news2024/9/24 21:21:22

0.关注博主有更多知识

C++知识合集

目录

1.再谈构造函数

1.1初始化列表

1.2初始化列表的初始化顺序

1.3构造函数的隐式类型转换

1.4explicit关键字

2.static成员

2.1static成员变量

2.2static成员函数

3.友元

3.1友元函数

3.2友元类

4.内部类

5.匿名对象

6.编译器对拷贝对象的一些优化

7.练习题

1.再谈构造函数

构造函数的功能是在对象定义时初始化其成员,但是我们以前的初始化方式是"错误"的:

class Date
{
public:
	Date(int year = 1,int month = 1,int day = 1)
	{
		// 这不是初始化,这是在赋值
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

即使这样做我们依然能够达到我们所谓的初始化效果,但我们需要注意,有些变量是必须在定义时给定初始值的,比如const变量、引用变量以及自定义类型对象。

1.1初始化列表

我们将上面的Date类修改成正确的成员初始化版本:

class Date
{
public:
	Date(int year = 1,int month = 1,int day = 1)
		:_year(year), _month(month), _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};

可以看到在构造函数的声明和函数体中间多了一个我们从未见过的东西,这个东西就叫做初始化列表。初始化列表的作用就是初始化成员变量,也就是说定义对象时调用构造函数,对象中的成员在构造函数的初始化列表当中初始化

初始化列表的格式为:以一个冒号开始,紧接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一对小括号,小括号里面放的内容就是初始化的值。同时需要注意,每个成员变量在初始化列表当中只能出现一次,也就是只能初始化一次。

初始化列表是一直存在的,不管是哪一类的构造函数当中,即使我们不显式的写初始化列表,它依然存在。这也就意味着类的所有成员变量(除了静态成员)在定义时都要经过初始化列表。这里再次强调一点,类当中的成员都是声明,只有实例化对象的时候才发生定义

那么对于普通成员变量来说,我们不在初始化列表当中显式初始化它们,它们就会被编译器用随机值来初始化:

#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1,int month = 1,int day = 1)
	{
		cout << _year << ":" << _month << ":" << _day << endl;
 	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	return 0;
}

我们之前说过编译器默认生成的构造函数初始化自定义类型的对象时,会调用它的默认构造。那么如今再完善一下这个说法:无论是哪种类型的构造函数,自定义类型对象都会经过初始化列表,如果初始化列表当中没有显式初始化该对象,就会调用其默认构造,如果没有默认构造,那么就报错:

class A
{
public:
	A(int a)// A类没有默认构造
		:_a(a)
	{}
private:
	int _a;
};

class Test
{
public:
	Test()
		:_x(3)// 初始化列表没有显式初始化对象,调用默认构造,但是A类没有默认构造,报错
	{}
private:
	const int _x;
	A _aa;// 自定义类型对象
};

int main()
{
	Test t1;
	return 0;
}

上面的内容看起来比较乱,在这里总结一下

  1.类实例化对象的时候,对象的成员被定义,被定义的地方在初始化列表

  2.对象的每个成员都会经过初始化列表,无论初始化列表有没有显式的写出来或者初始化列表当中没有显式初始化该成员

  3.如果显式的在初始化列表当中显式初始化成员了,那么就用显式写的初始化方法初始化成员

  4.如果没有显式的在初始化列表当中初始化成员,就分为两种情况:

    1)如果成员为内置类型,那么就用随机值初始化

    2)如果为自定义类型,就会调用它的默认构造,如果没有默认构造,那么就报错

  5.C++11给出了一种特殊情况,即成员的缺省值,既然作为缺省值这就验证了类当中的成员是声明而不是定义。那么在初始化列表当中,如果我们没有显式初始化成员,就会检查该成员有没有缺省值,如果有缺省值,就用缺省值初始化该成员;当然,不想使用缺省值时也可以显式的在初始化列表当中初始化:

#include <iostream>
#include <cstdlib>
using namespace std;
class Test
{
public:
	Test()
		:_y(30)
		, _a((int*)malloc(100))// 只是演示一下可以这么完,因为这里相当于int* _a = (int*)malloc(100)
	{
		cout << "_x = " << _x << " _y = " << _y << endl;
	}
private:
	int _x = 3;
	int _y = 5;
	int* _a;
};

int main()
{
	Test t1;
	return 0;
}

当类中包含以下任意一个成员时,它必须在初始化列表进行初始化

  1.const成员变量:哪怕我们将const变量不作为成员变量,在外部定义时不给初值也是会报错的:

int main()
{
	const int x;// 不给初值
	return 0;
}

  那么const变量正确定义之后是不能被赋值的:

int main()
{
	const int x = 100;
	x = 300;// 错误
	return 0;
}

  所以在类中,const成员变量必须在构造函数中的初始化列表显式初始化:

class Test
{
public:
	Test()
		:_x(3)
	{}
private:
	const int _x;
};

  2.引用变量:我们已经证实过了引用在定义时不给初值是会报错的(如果忘了的朋友赶紧去我的主页点开C++的Chapter1),所以当引用变量作为类的成员时,必须在构造函数中的初始化列表显式初始化:

double val = 3.14;// 全局变量
class Test
{
public:
	Test()
		:_x(3)
		, _rf(val)// 引用全局变量
	{}
private:
	const int _x;
	double& _rf;
};

  3.没有默认构造函数的自定义类型对象:如果不在初始化列表当中显式初始化,那么就会调用默认构造函数,但是如果对象没有默认构造函数,就会报错。所以没有没有默认构造函数的自定义类型对象必须在初始化列表当中显式初始化:

class A
{
public:
	A(int a)
		:_a(a)
	{}
private:
	int _a;
};

class Test
{
public:
	Test(int x)
		:_aa(x)// A类没有默认构造,必须显式初始化
	{}
private:
	A _aa;
};

int main()
{
	Test t1(20);
	return 0;
}

由此可见,初始化列表是相当重要的,也就是说,能在初始化列表当中初始化的成员,尽量在初始化列表当中初始化。 

1.2初始化列表的初始化顺序

初始化列表当中出现的显式初始化,不是真实的初始化顺序,真实的初始化顺序与成员的声明顺序有关(我也不知道那帮大佬为什么要这样设计):

#include <iostream>
using namespace std;
class Date
{
public:
	Date()
		:_month(5), _day(_month), _year(2023)
	{
		cout << _year << ":" << _month << ":" << _day << endl;
	}

private:
	int _year;
	int _day;
	int _month;
};
int main()
{
	Date d1;
	return 0;
}

可以看到我用5去初始化_month,然后我的本意是初始化一个5月5日出来,但是打印结果确实上面那样。事实上,无论初始化列表的初始化是怎么写的,最终的初始化顺序都是按照成员声明的顺序来的。在这个例子中,_year是最先声明的,所以最先使用2023初始化_year;其实是_day,所以会先使用_month的值初始化_day,但是因为_month还没有被初始化,所以_month当前是一个随机值,所以_day也是一个随机值;最后声明的便是_month,所以最后使用5去初始化_month。所以最后的结果就是_year=2023、_month=5、_day=随机值。

1.3构造函数的隐式类型转换

在C++11之前,即C++98当中,支持单参数的构造函数的隐式类型转换。注意,这个单参数不是指构造函数只有一个参数(只有一个参数的话那就只有一个this指针了),单参数指的构造对象时只需要给构造函数传递一个参数就可以实例化出对象的构造函数。嗯...非常不好理解,我们看一段匪夷所思的代码:

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year), _month(month), _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	// 常规的构造
	Date d1(2023, 5, 3);
	Date d2(2023);

	// 隐式类型转换的构造
	Date d3 = 2025;
	const Date& rd = 2025;
	return 0;
}

这段代码是不会产生任何编译错误的。我们可以看到d3使用一个常量整数2025来初始化,常引用rd引用了一个常量整数2025,既然我们说了这是一种隐式类型转换,那么它一定会产生临时变量,所以"Date d3 = 2025"可以被解释为int类型强制类型转换成Date类型,具体的工作就是生成一个临时对象,该临时对象用2025来初始化,最后该临时对象拷贝给正在创建的d3,所以d3对象是用一个已经存在的临时对象来拷贝构造初始化的:

  那么对于常引用rd来说也是类似的:

可以看到隐式类型转换构造对象时,会发生一次构造和一次拷贝构造,我们以一段代码来验证:

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year), _month(month), _day(day)
	{
		cout << "构造函数" << endl;
	}
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		cout << "拷贝构造" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d3 = 2025;
	return 0;
}

但是最终的输出结果只有一个构造函数,是我们的理论错了吗?实际上这是编译器优化的结果,因为编译器认为构造函数的隐式类型转换是一次脱裤子放屁的过程,先构造出临时对象,再用临时对象拷贝构造给正在创建的对象,编译器一看,觉得麻烦,索性直接越过了拷贝构造这个过程,所以上面的输出结果只有构造函数被调用,所以构造函数的隐式类型转换被优化成了只发生构造,不发生拷贝构造。当然了,这个编译器一定是现代流行的编译器,十几年前的老古董可能不会做这种优化。

这种单参数的构造函数的隐式类型转换C++98就已经支持了,那么可想而知C++11一定对这个点进行了扩展,即支持了多参数的构造函数的隐式类型转换,我们看一段代码:

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year), _month(month), _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	//Date d1 = 2023,5,3;// 错误写法
	//Date d2 = (2023, 5, 3);// 错误写法
	Date d3 = { 2023, 5, 3 };// 正确写法
	return 0;
}

可以看到,在C++11当中,可以将构造函数所需的参数用一对大括号给圈起来,然后发生类对象构造时的隐式类型转换。

那么这种隐式类型转换有什么作用呢?提前透露一下后面的内容,我们在后面将会介绍string类,其中string类有这么一个构造函数:

  即string类的对象可以用一个指针构造,那么我们举个例子:

#include <iostream>
#include <string>// string类的头文件
using namespace std;

int main()
{
	//const char* str = "hello";
	//string s = str;

	string s = "hello";// 与上面的写法等价
	cout << s << endl;// string也重载了<<
	return 0;
}

  上面的代码不是关键,关键是假设有一名为func的函数,其参数是一个string类型,那么我们就可以这么玩了:

void func(string s)
{}
int main()
{
	func("hello");// 只需要这么去传,并且只发生构造
	
	// 不需要这样写了
	//string s("hello");//一次构造
	//func(s);//一次拷贝构造
	return 0;
}

  可以看到,构造函数的隐式类型转换还是有点作用的,再加上编译器的优化,使其不需要发生拷贝构造,所以这是一种高效、方便的代码编写手段。

1.4explicit关键字

在一些特定的设计场景当中,不希望发生构造函数的隐式类型转化,我们就可以在构造函数的声明之间加上explicit关键字:

class Date
{
public:
	// 限制了隐式类型转换的发生
	explicit Date(int year = 1, int month = 1, int day = 1)
		:_year(year), _month(month), _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1 = { 2023, 5, 3 };
	Date d2 = 2024;
	return 0;
}

2.static成员

首先实现一个小小的需求,即设计一个类,计算程序中创建了多少个该类的对象。首先我们要明白,对象的创建无非就两种方式,要么是通过构造函数,要么是通过拷贝构造,所以我们可以定义一个全局变量,来计算创建了多少个对象:

int N = 0;
class A
{
public:
	A()
	{
		++N;
	}
	A(const A& a)
	{
		++N;
	}
};
int main()
{
	A a1;
	A a2 = a1;
	cout << N << endl;
	return 0;
}

这种方法虽然能够实现我们的需求,但是是不好的。因为N作为全局变量,从操作系统的角度来看,该变量能够被该进程当中的所有线程共享,也就是说所有线程都可以对该变量进行修改,而且不需要什么成本。总之一句话,这种做法是不安全的。

2.1static成员变量

使用static修饰类的成员变量时,我们就称该成员为static成员变量或者静态成员变量。我们知道,static修饰的局部变量修改其生命周期,使其声明周期延长到了全局,并且保存在数据段(静态区)中。那么静态成员变量也不例外,它也存储在数据段中,也就是说,静态成员变量不存储在对象当中,静态成员变量是所有对象共享的,该静态成员变量在所有对象当中只有一份,即静态成员变量属于整个类。同时,因为静态成员变量不属于任何一个对象,所以它的定义不能被放在构造函数或者拷贝构造当中,它必须在类外定义。我们将上面的全局变量N作为A类的静态成员变量:

class A
{
public:
	A()
	{
		++N;
	}
	A(const A& a)
	{
		++N;
	}
public:// 注意这里是公有
	static int N;
	//static int N = 0;// 错误的写法,不能给缺省值,因为静态成员变量不在构造函数中定义
};

// 静态成员变量在类外定义
//int N = 0;//这种写法是单独定义一个全局变量
int A::N = 0;// 指明类域

int main()
{
	A a1;
	A a2 = a1;
	cout << A::N << endl;// 访问时只需指明类域即可
	cout << a1.N << endl;// 静态成员变量在对象当中是共享的,所以也可以访问

	A* pa = nullptr;
	cout << pa->N << endl;// 我们说过对象的作用只是为了指明类域
	return 0;
}

注意,A类的所有成员的访问限定符都是公有的,可以直接通过类域访问。

2.2static成员函数

如果我们将上面A类中的静态成员变量N的访问限定符修改为private,那么就无法通过类域指明直接访问。我们仍然有一招可用,那就是给A类再定义一个成员函数,这个成员函数的作用就是返回静态成员变量的值:

class A
{
public:
	A()
	{
		++N;
	}
	A(const A& a)
	{
		++N;
	}
	
	int GetN()
	{
		return N;
	}
private:
	static int N;
};

int A::N = 0;

int main()
{
	A a1;
	A a2 = a1;
	// 不能直接访问私有
	//cout << A::N << endl;
	//cout << a1.N << endl;

	cout << a1.GetN() << endl;
	cout << a2.GetN() << endl;

	A* pa = nullptr;
	cout << pa->GetN() << endl;// GetN()内部没有发生this指针的解引用

	//cout << A::GetN() << endl;// 错误
	return 0;
}

可以看到,普通成员函数的访问必须通过对象,无法直接指明类域而进行访问。那么注意到上面代码中的一个注释,即"GetN()内部没有发生this指针的解引用",这是为什么?我们明明在GetN()函数当中使用了静态成员变量N。这里需要说明一下,静态成员变量它不属于任何一个对象,而是属于整个类,既然不属于任何一个对象那么this指针当然访问不到它,既然访问它时根本不需要this指针,因为静态成员变量在类域当中是所有成员函数都可见的

那么既然访问静态成员变量不需要this指针的参与,那么可以将GetN()设计成静态成员函数,静态成员函数的特性之一就是没有this指针:

class A
{
public:
	A()
	{
		++N;
	}
	A(const A& a)
	{
		++N;
	}
	
	static int GetN()// 静态成员函数
	{
		return N;
	}
private:
	static int N;
};

int A::N = 0;

int main()
{
	A a1;
	A a2 = a1;

	cout << a1.GetN() << endl;
	cout << a2.GetN() << endl;

	A* pa = nullptr;
	cout << pa->GetN() << endl;

	cout << A::GetN() << endl;
	return 0;
}

因为静态成员函数没有this指针,所以它可以直接指明类域访问,这是静态成员函数和普通成员函数的区别之一。

那么最后总结一下静态成员变量和静态成员函数的特性:

  1.静态成员变量是所有对象所共享的,但是它不属于任何一个成员,存放在数据段中

  2.静态成员变量的定义和初始化必须放在类外

  3.静态成员可以通过[类名::静态成员]或者[对象.静态成员]或者[对象指针->静态成员]的形式访问

  4.静态成员函数没有this指针,这就注定了静态成员函数无法访问任何非静态成员

  5.静态成员也是类的成员,它们也受访问限定符的限制

3.友元

友元是一种突破类的封装的方式,在某些情况下能够提供遍历,但是友元会增加代码的耦合度、破坏封装,所以能不用尽量不用。友元关系分为友元函数和友元类

3.1友元函数

在上篇博客当中已经介绍过友元函数了,就是流插入运算符和流提取运算符重载的日期类例子。友元函数的定义非常简单,只需要在类的任意位置声明函数,并在其声明之前加上friend关键字即可。

友元函数的作用就是可以直接访问类的私有成员使用友元函数时必须清楚以下几个概念

  1.友元函数可以直接访问类的私有和保护成员,但是友元函数不是类的成员函数

  2.友元函数不能定义为const成员函数,因为const不是修饰函数的,修饰的是this指针,而友元函数不是类的成员函数所有没有this指针

  3.友元函数的声明可以放在类的任何地方,并且不受访问限定符的限制(声明为私有、保护、公有都没有区别)

  4.一个函数可以是多个类的友元函数,也就是说友元函数可以访问多个类的私有、保护成员

  5.调用友元函数与普通全局函数的调用没有区别

3.2友元类

友元类的使用方法和友元函数一样,只需要在类中声明类,并且在声明之前加上friend关键字即可:

class A
{
	/*在类中声明B类是A类的友元类*/
	friend class B;
public:
private:
	int _x = 1;
	int _y = 2;
};

class B
{
public:
private:
	A _aa;
	int _z;
};

那么理所应当,友元类可以访问类中的私有、保护成员:

class A
{
	/*在类中声明B类是A类的友元类*/
	friend class B;
public:
private:
	int _x = 1;
	int _y = 2;
};

class B
{
public:
	/*
	 *_aa对象调用它的默认构造
	 *直接在A类之外使用A类的私有成员 
	*/
	B()
	{
		_z = _aa._x;
	}
private:
	A _aa;
	int _z;
};

同时需要注意友元关系的使用规则

  1.友元关系是单向的,不具有交换性。例如B是A的友元,那么B可以访问A的私有、保护成员,但是A不能访问B的私有、保护成员

  2.友元关系不能传递,例如C是B的友元,B是A的友元,但是C依然不能访问A的私有、保护成员

  3.友元关系不能被继承(后面再说)

4.内部类

在类中再定义一个新类,这个新类就是内部类:

class A
{
public:
	/*在A类中定义一个新类*/
	class B
	{
	public:
	private:
		int _z;
	};

private:
	int _x = 1;
	int _y = 2;
};

以上面这段代码为例,我们可以计算一下A类的大小是多少:

int main()
{
	cout << sizeof A << endl;
	return 0;
}

可以看到打印的结果与我们的直觉相违背,因为A类当中有3个int类型的成员变量,最后的结果应该是12。实际上我们的想法是错误的,在类中定义一个新类,这两个新类之间没有任何关系,即内部类与外部类没有任何关系,它们是两个独立的类。那么内部类的作用是什么?内部类天生就是外部类的友元,内部类可以直接访问外部类的任何成员:

class A
{
public:
	/*内部类可以访问外部类的任何成员
	 *在类当中不能定义当前类的对象,只能定义指针或引用
	 *例如A a是错误的,A *a,A &ra则正确*/
	class B
	{
	public:
		void func(const A& aa)
		{
			cout << aa._x << " " << aa._y << endl;
		}
	};

private:
	int _x = 1;
	int _y = 2;
};

int main()
{
	/*实例化A类型的对象不会实例化出B类型的对象
	 *因为它们两个是独立的类*/
	A a1;
	A::B b;
	b.func(a1);
}

既然内部类天生是外部类的友元,并且它们两个之间没有任何关系,那么它们与下面这样的写法没有区别:

class A
{
public:
	friend class B;
private:
	int _x = 1;
	int _y = 2;
};

class B
{
public:
	void func(const A& aa)
	{
		cout << aa._x << " " << aa._y << endl;
	}
};

但是这并不代表内部类没有用处,相反,存在即合理。那么内部类的具体作用我这里先卖个关子,在本篇博客的末尾将会有一道习题,其中就囊括了内部类的正确打开方式。

5.匿名对象

匿名对象就是没有名字的对象,实际上我们接触过匿名对象,那就是传值返回当中生成的临时对象、构造函数隐式类型转换当中生成的临时对象。当然,我们还可以显式定义匿名对象:

class A
{
public:
	A(int x = 1, int y = 1)
	{}

	void testA(int n)
	{
		cout << n << endl;
	}
};

int main()
{
	/*想要访问某个成员函数必须通过对象
	 *如果仅仅是为了访问某个成员函数而创建对象就有点浪费了
	 *不是浪费什么资源,而是写起来麻烦*/
	A a1;
	A a2(100);
	A a3(100, 200);
	a1.testA(10);
	a2.testA(100);
	a3.testA(1000);

	/*匿名对象可以在一行当中完成对象的实例化和函数调用*/
	A().testA(20);
	A(200).testA(200);
	A(200, 300).testA(2000);

	return 0;
}

匿名对象的定义其实很简单,就是定义对象时别给对象起名就行。那么因为匿名对象没有名字,所以它的生命周期不跟随对象所在的作用域,而是当前行,即匿名对象的生命周期只有当前行

class A
{
public:
	~A()
	{
		cout << "~A()" << endl;
	}
};

int main()
{
	A();
	return 0;
}

匿名对象主要是方便传参和传值返回

A func(const A& a)
{
	/*如果单纯为了返回一个对象,这样写就行了*/
	return A();
	//A ret;
	//return ret;
}

int main()
{
	/*如果只是为了传参,那么没必要这么写*/
	//A a;
	//func(a);

	/*匿名对象即可(减少代码量[手动狗头])*/
	func(A());
	return 0;
}

当然,"减少代码量"不是主要的原因,更重要的是能够触发编译器的优化机制

6.编译器对拷贝对象的一些优化

在这之前介绍过构造函数的隐式类型转换,其中本应发生一次构造和一次拷贝构造,最后被编译器优化成了只发生一次构造。那么像这样在一行代码当中发生连续的构造、拷贝行为(包括函数调用),编译器有可能会对它们做出优化。优化行为通常发生在构造函数的隐式类型转换、传值传参、传值返回当中。下面我们依次介绍(隐式类型转换介绍过了):

  以下所有的测试都使用这个类:

class A
{
public:
	/*发生构造、拷贝构造时打印一下*/
	A()
	{
		cout << "A()" << endl;
	}
	A(const A& a)
	{
		cout << "A(const A& a)" << endl;
	}
};

  1.传值传参:首先来看第一种场景:

void func(A a)
{}
int main()
{
	A a;
	func(a);
	return 0;
}

  可以看到输出结果,证明编译器并没有帮我们做出优化,原因就在于不符合优化的条件(一行代码中发生连续的构造、拷贝构造行为)。那么们使用匿名对象,使条件符合:

void func(A a)
{}
int main()
{
	func(A());
	return 0;
}

  可以看到,调用func()函数的实参为一个匿名对象,匿名对象实例化时需要调用拷贝构造,那么打印结果打印构造是合理的。那么调用func()函数的实参既然是一个对象,传值传参时,应当发生一次拷贝构造,但是打印结果并没有打印,可以证明,编译器对其进行了优化。也就是说,在这个例子当中,func()函数的形参a本来需要拷贝构造的,但是编译器觉得有点"脱裤子放屁",直接将调用func()函数的实参交给形参构造(A类的构造函数没有参数)。

  2.传值返回:先来看第一种场景:

A func()
{
	A a;
	return a;
}
int main()
{
	A ret = func();
	return 0;
}

  按道理来说,在func()函数内部定义一个对象发生一次构造,执行return语句时生成一个临时对象发生一次拷贝构造,最后外部接收func()返回值的时候又会发生一次拷贝构造。但是输出结果证明了一件事,编译器做出了优化。优化的原因还是因为"脱裤子放屁",因为编译器认为,将返回值拷贝给临时对象,临时对象又拷贝给外部接收返回值的对象,那这多麻烦啊,不如直接将返回值拷贝给外部接收返回值的对象:

  因为func()函数内部并没有发生一行代码中连续构造、拷贝的行为,所以我们现在使用匿名对象做返回值,可以触发编译器的极致优化:

A func()
{
	return A();
}
int main()
{
	A ret = func();
	return 0;
}

  因为匿名对象的生命周期只有定义的那一行,正常的过程应该是构造、拷贝构造给临时对象,临时对象拷贝构造给外部接收返回值的对象,其中临时对象拷贝给外部对象发生的拷贝构造的优化是必然的,而又因为匿名对象只有一行的生命周期,所以编译器索性直接把它也干掉了:

7.练习题

JZ64 求1+2+3+...+n

这道题限制了我们不能使用常规手段来解题,那么就需要使用"奇技淫巧"了。我们试想,以前学过的知识哪里可以与这道题产生关联?那就是静态成员,我们可以定义一个类,类中有两个静态成员,其中一个便是加数,另一个便是最终求的和。我们利用实例化对象时会自定调用构造函数的特性来解决这道题:

/*定义一个类*/
class Result
{
public:
    Result()
    {
        _sum += _add;
        ++_add;
    }
    static int getSum()
    {
        return _sum;
    }
private:
    static int _add;
    static int _sum;
};
/*题目要求从1加到n*/
int Result::_add = 1;
int Result::_sum = 0;

class Solution {
public:
    int Sum_Solution(int n) {
        /*如何不用循环定义多个对象?数组*/
        Result arr[n];
        return Result::getSum();
    }
};

另外,我们还可以将Result类作为Solution类的内部类,体会一下内部类的作用:

class Solution 
{
/*Result作为Solution的私有内部类
 *因为Result的目的就是为了帮助Solution解决问题
 *Result类仅被Solution利用,所以作为私有*/
private:
    class Result
    {
    public:
        Result()
        {
            _sum += _add;
            ++_add;
        }
        static int getSum()
        {
            return _sum;
        }
    };
public:
    int Sum_Solution(int n) {
        Result arr[n];
        return Result::getSum();
    }
private:
    static int _add;
    static int _sum;
};

int Solution::_add = 1;
int Solution::_sum = 0;

HJ73 计算日期到天数转换

这道题不难想到肯定与日期类有关,我们可以直接将我们以前所写的日期类直接拷贝过去,然后定义对象,然后两个对象相减即可得出结果(例如2023 5 6构造的对象利用运算符重载减去2023 1 1构造的对象),但是我们不那么做,我们使用一种更加简单的方法。因为每一年的天数都是固定的,要么是365天要么是366天,那么我们完全可以手动计算日期的天数和:

#include <iostream>
using namespace std;

int main()
{
    int year = 0,month = 0,day = 0;
    cin >> year >> month >> day;
    /*一月有31天,1月到2月一共有59天,1月到3月一共有90天......*/
    int getMonthSum[13] = {0,31,59,90,120,151,181,212,243,273,304,334,365};

    /*先求除当前月之前的所有月天数和,再加上day*/
    int res = getMonthSum[month-1] + day;
    
    /*判断是否为闰年,如果是闰年,天数+1
     *需要注意一定+1的条件一定2月之后*/
    if(month > 2 && ((year % 4 == 0 && year % 100 != 0 ) || (year % 400 == 0)))
    {
        ++res;
    }
    cout << res << endl;
    return 0;
}

KY111 日期差值

这道题是有点难度的(对于我来说),所以我直接暴力解法,将以前所写的日期直接复制过去:

/*这里复制了整个日期类*/


int main()
{
    int d1 = 0,d2 = 0;
    cin >> d1 >> d2;

    /*将整数的年、月、日分别取出来*/
    int d1_day = d1 % 100,d1_month = d1 % 2000 / 100,d1_year = d1 / 10000;
    int d2_day = d2 % 100,d2_month = d2 % 2000 / 100,d2_year = d2 / 10000;
    
    Date date1(d1_year,d1_month,d1_day);
    Date date2(d2_year,d2_month,d2_day);

    cout << (date1 - date2)+1 << endl;
   
    return 0;
}

KY222 打印日期

#include <iostream>
using namespace std;

int main()
{
    int year = 0,day = 0;
    while(cin >> year >> day)
    {
        int getMonthDay[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31};
        if((year % 4 == 0 && year % 100 != 0 ) || (year % 400 == 0))
        {/*如果是闰年,2月天数变一下*/
            getMonthDay[2] = 29;
        }

        /*从一月开始,如果day大于当前月的天数
         *day-=当前月的天数,当前月加1*/
        int month = 1;
        while(day > getMonthDay[month])
        {
            day -= getMonthDay[month++];
        }

        printf("%04d-%02d-%02d\n",year,month,day);
    }
    return 0;
}

KY258 日期累加

#include <iostream>
using namespace std;

int getMonthDay(int year, int month)
{
    int day[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

    // 如果是2月又是闰年
    if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
    {
        return 29;
    }
    return day[month];
}

int main()
{
    int m = 0;
    cin >> m;
    while(m--)
    {
        int year = 0,month = 0,day = 0,num = 0;
        cin >> year >> month >> day >> num;

        day += num;
        while(day > getMonthDay(year,month))
        {
            day -= getMonthDay(year,month);
            ++month;
            if(month == 13)
            {
                ++year;
                month = 1;
            }
        }

        printf("%04d-%02d-%02d\n",year,month,day);
    }
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/498306.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

美颜sdk对于移动端视频直播的优化效果研究报告

随着移动互联网的快速发展&#xff0c;移动端视频直播应用也越来越受到用户的青睐。然而&#xff0c;对于许多用户来说&#xff0c;直播的画质却成为了一个令人头疼的问题。为了解决这个问题&#xff0c;许多直播应用开始引入美颜sdk&#xff0c;以期提升直播画质和用户体验。本…

凌思微-蓝牙框架-流程理解

1.蓝牙SOC芯片主函数流程 int main() { sys_init_app(); ble_init(); dev_manager_init(dev_manager_callback); gap_manager_init(gap_manager_callback); gatt_manager_init(gatt_manager_callback); rtos_init(); ble_task_init(); app_task_init(); vTaskStartScheduler();…

第8章:聚合函数

目录 一、常见的聚合函数 二、GROUP BY 的使用 三、HAVING 的使用&#xff0c;过滤数据 四、SQL底层的执行原理 五、练习 一、常见的聚合函数 1.概念 聚合函数作用于一组数据&#xff0c;并对一组数据返回一个值。 2.聚合函数的类型 AVG(),SUM(),MAX(),MIN(),COUNT() 3. AV…

【Spring篇】AOP

&#x1f353;系列专栏:Spring系列专栏 &#x1f349;个人主页:个人主页 目录 一、AOP简介 1.什么是AOP? 2.AOP作用 3.AOP核心概念 二、AOP入门案例 1.需求分析 2.思路分析 3.环境准备 4.AOP实现步骤 三、AOP工作流程 1.AOP工作流程 2.AOP核心概念 四、AOP配置管…

Python小姿势 - 1. Python的设计理念

Python的设计理念 Python的设计理念是“优雅”、“明确”、“简单”。 优雅&#xff1a;Python代码风格优美&#xff0c;语法简洁明了&#xff0c;代码可读性高&#xff0c;易于理解和维护。 明确&#xff1a;Python语言规范清晰&#xff0c;标准库丰富&#xff0c;可用于开发各…

第五章 作业(123)【编译原理】

第五章 作业【编译原理】 前言推荐第五章 作业123 随堂练习课前热身04-17随堂练习04-17课前热身04-24 最后 前言 2023-5-3 22:12:46 以下内容源自《【编译原理】》 仅供学习交流使用 推荐 第四章 作业&#xff08;123&#xff09;【编译原理】 第五章 作业 1 1.令文法G为…

医生的百科词条怎么创建?医生的百科词条创建技巧值得你收藏

医生是医学领域中的专业人员&#xff0c;主要负责诊断、治疗和预防疾病。在现代社会中&#xff0c;医生的角色越来越重要&#xff0c;是社会中不可或缺的职业之一。 而随着互联网的发展&#xff0c;百度百科已成为人们获取信息的重要途径&#xff0c;医生想要提高自己的知名度和…

SpringBoot项目简单入门

一、创建项目 1、选择Spring Initializr 2、为了提高项目构建效率&#xff0c;可以尝试修改阿里脚手架&#xff0c;地址如下&#xff1a; https://start.aliyun.com 3、点击下一步 4、选择Web与spring Web&#xff0c;然后点击完成开始项目构建 5、项目构建完成如图所示 二、…

Linux基础知识—Linux

文章目录 1.认识Linux2.常见命令2.1ls2.2pwd2.3cd2.4touch2.5mkdir2.6rm2.7cp2.8mv2.9man2.10date2.11grep2.12ps2.13netstat 3.文件内容的操作3.1cat3.2vim3.3less3.4head3.5tail3.6管道|3.7重定向 4.管理软件4.1yum&#xff08;在线的方式管理&#xff09;4.2rpm&#xff08;…

OnlineJudge-负载均衡式在线OJ

关于个人项目是在找实习以及参加秋招非常重要的简历内容&#xff0c;今天博主来介绍一下自己的一个项目。 开发环境&#xff1a;CentOS7、Makefile、g、vscode、MySQL Workbench 所用技术&#xff1a;C STL 标准库、Boost 准标准库(字符串切割)、cpp-httplib 第三方开源网络库 …

数据结构(C语言):两个字符串比较大小

一、一个小插曲 在写这篇文章之前&#xff0c;作者想先和大家分享一个小故事。如果你不想看这个小故事的话&#xff0c;可以直接跳到第二点哦。 为了锻炼自己的编码能力&#xff0c;平时作业和实验题的代码我都是不看书、不看老师的PPT&#xff0c;按照自己的思路一行一行敲出…

【STM32CubeMX】F103RTC时钟

前言 本文记录了我学习STM32CubeMX的过程&#xff0c;方便以后回忆。我们使用的开发板是基于STM32F103C6T6的。本章记录了RTC时钟的基础配置。下文调试时用到的串口来查看&#xff0c;不过串口的配置省略了。 步骤 实验目标&#xff1a;基于RTC时钟&#xff0c;查看它的秒计时…

Mac电脑配置李沐深度学习环境[pytorch版本]使用vscode

文章目录 第一步 M1芯片安装Pytorch环境安装Miniforge创建虚拟环境安装Pytorch 第二步 下载李沐Jupyter文件第三步 配置vscode参考 第一步 M1芯片安装Pytorch环境 安装Miniforge Mac打开终端&#xff08;Mac电脑如何启动终端&#xff1f;打开启动台&#xff0c;搜索终端即可&…

网络安全合规-数据分类分级标准汇编

今天主要学习讲解的是网络安全合规-数据分类分级标准汇编。 作为数据安全治理的前期首要工作-分类分级&#xff0c;而分类分级的开展工作又是根据相关标准开展的&#xff0c;建立数据安全防护体系的第一步就是梳理数据资产进行分类分级。只有做好分类分级工作&#xff0c;对不同…

迈向多模态AGI之开放世界目标检测 | 人工智能

作者&#xff1a;王斌 谢春宇 冷大炜 引言 目标检测是计算机视觉中的一个非常重要的基础任务&#xff0c;与常见的的图像分类/识别任务不同&#xff0c;目标检测需要模型在给出目标的类别之上&#xff0c;进一步给出目标的位置和大小信息&#xff0c;在CV三大任务&#xff08;识…

GIMP制作艺术字技巧

GIMP下载官网 https://www.gimp.org/downloads/ 我使用的版本 2.10.32 字体下载 https://ziyouziti.com/index-index-all.html 下载解压之后会有otf、ttf等字体文件&#xff0c;需要拷贝到gimp当前用户目录 C:\Users\用户名\AppData\Roaming\GIMP\2.10\fonts GIMP绘制字…

分布式医疗云平台【项目简介、适合对象、技术选型、项目的核心功能模块 、模块设计及功能演示】(一)-全面详解(学习总结---从入门到深化)

目录 分布式医疗云平台 一、项目简介 二、适合对象 三、技术选型 四、项目的核心功能模块 五、项目特色 六、模块设计及功能演示 分布式医疗云平台 一、项目简介 分布式医疗云平台系统是以完整的基层医疗机构信息化解决方案为出发点&#xff0c;打造链接诊所、医生、…

【P7】JMeter 计数器

&#xff08;1&#xff09;、测试计划右键 <<< 添加 <<< 配置元件 <<< 计数器 Starting value&#xff1a;1 递增&#xff1a;1 Maximum value&#xff1a;9999 数字格式&#xff1a;var_0000 引用名称&#xff1a;var &#xff08;2&#xf…

web集群,部署jpress应用

1.静态网页与动态网页的区别 静态网页&#xff1a; &#xff08;1&#xff09;请求响应信息&#xff0c;发送给客户端进行处理&#xff0c;由浏览器进行解析&#xff0c;显示页面称为静态页面。在网站设计中&#xff0c;纯粹html格式的网页&#xff08;包含图片&#xff0c;视…

學習日記,java与写题目

开篇来个每日一题 1419. 数青蛙 难度中等185收藏分享切换为英文接收动态反馈 给你一个字符串 croakOfFrogs&#xff0c;它表示不同青蛙发出的蛙鸣声&#xff08;字符串 "croak" &#xff09;的组合。由于同一时间可以有多只青蛙呱呱作响&#xff0c;所以 croakOfF…