【C++】类和对象【下篇】--初始化列表,static成员,友元,内部类,匿名对象


失败是什么?没有什么,只是更走近成功一步。成功是什么?就是走过了所有通向失败的路。只剩下一条路,那就是成功的路

一、再谈构造函数

1.构造函数体赋值

我们在类和对象中我们学习到在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值,就比如下面的日期类

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

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。

我们知道类里面只是成员变量的声明,并不是定义成员变量,因为类并不会占用空间,就好比我们设计房子时的图纸,只有我们在真正修建房子的时候才会占用空间,类也一样,只有我们用类实例化出具体的对象的时候才会对成员变量进行定义,而对象是整体定义的,那么对象中具体的每一个成员变量在那么定义呢?

C++类对象中的成员变量在初始化列表进行初始化。

2.初始化列表

1.概念

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

我们还是以日期类为例:

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 d1(2023, 1, 1);
	return 0;
}

在这里插入图片描述

2.特性

初始化列表有以下几个特性:

1.初始化列表是每个成员变量定义和初始化的地方,所以每个成员变量(内置类型和自定义类型),无论我们是否显示在初始化列表出写,都一定会走初始列表,并且初始化操作只能进行一次,即每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

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

int main()
{
    
    
	Date d1(2023, 1, 1);
	return 0;
}

在这里插入图片描述

我们可以看到,我们对_day进行两次初始操作的时候编译器会报错,因为变量只能初始化一次。

2.如果我们在初始化列表显示写了编译器就会用显示写的来初始化,如果我们没有在初始化列表显示的写,那么对于内置类型,有缺省值用缺省值,没有编译器就会使用随机值来初始化,对于自定义类型,编译器会调用自定义类型的默认构造函数来初始化,如果没有默认构造编译器就会报错

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

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

int main()
{
    
    
	Date d1(2023, 1, 1);
	return 0;
}

在这里插入图片描述

我们可以看到我们没有在初始化列表里显示对_day和A aa进行初始化,_day为内置类型,被初始化为一个随机值,而aa为自定义类型,调用了自身的默认构造来初始化。

如果自定义类型既没有在初始化列表显示定义,也没有默认构造函数,编译器就会报错。

在这里插入图片描述

3.类中包含以下成员(引用,const,没有默认构造的自定义成员),必须放在初始化列表位置进行初始化:

引用成员变量

const成员变量

自定义类型成员(且该类没有默认构造函数时)

我们知道,引用一个变量的别名,它必须在定义的时候初始化,并且一旦引用了一个变量就不能再引用另一个变量,同时,const作为只读常量,也必须在定义的时候初始化,并且初始化初始化之后不能修改。自定义类型成员且没有默认构造,对于自定义类型,编译器会调用自定义类型的默认构造函数来初始化,如果没有默认构造编译器就会报错,所以也需要在初始化列表显示初始化。我们知道,构造函数的函数体内执行的是赋值语句,成员变量只能在初始化列表进行定义和初始化。

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

class B
{
    
    
public:
	B(int a, int ref, int n)
	{
    
    
		_aobj = a;
		_ref = ref;
		_n = n;
	}
private:
	A _aobj;   //没有默认构造函数
	int& _ref; //引用
	const int _n; // const 
};

int main()
{
    
    
	B b;
	return 0;
}

在这里插入图片描述

所以对于使用const修饰,引用类型的成员变量,已经没有默认构造的自定义类型成员,我们都需要在初始化列表对其进行初始化,否则编译器就会报错。

所以上面的代码正确的写法是:

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

class B
{
    
    
public:
	B(int a = 0, int ref = 0, int n = 10)
		:_aobj(a)
		,_ref(ref)
		,_n(n)
	{
    
    }
private:
	A _aobj;   //没有默认构造函数
	int& _ref; //引用
	const int _n; // const 
};

int main()
{
    
    
	B b;
	return 0;
}

此外,构造函数的初始化列表和函数体可以配合起来使用,即可以让初始化列表分别完成一部分,比如下面的代码:

// 初始化列表和函数体内初始化可以混着来
Stack(int capacity = 4)
	: _top(0)
	, _capacity(capacity)
{
    
    
	cout << "Stack(int capacity = 4)" << endl;

	_a = (int*)malloc(sizeof(int) * capacity);
	if (_a == nullptr)
	{
    
    
		perror("malloc fail");
		exit(-1);
	}
	memset(_a, 0, sizeof(int) * capacity);
}

4.尽量使用初始化列表进行初始化,因为无论我们是否在初始化列表显示初始化,类的成员函数在初始化的时候都会先使用初始化列表进行初始化

在这里插入图片描述

我们可以看到,即使我们显示定义的构造函数什么也没有写,_pushST和_popST也完成了初始化工作,因为无论我们是否在初始化列表显示写,类的成员变量都会走初始化列表,类的自定义类型的成员会去调用它的默认构造来完成初始化工作。

5.在C++11中对于内置类型打了一个补丁–内置类型成员变量可以在声明的时候给一个缺省值,其可以在初始化列表起作用

我们之前在学习构造函数的时候,并不知道初始化列表的存在,所以认为默认生成的构造函数对内置类型不做处理,而C++11为了弥补这个缺陷,打了一个补丁,即可以在成员变量声明的时候给一个缺省值,现在我们知道,内置类型也会在初始化列表进行初始化,只是因为初始化的是一个随机值,感觉就行没有初始化一样,所以成员函数的缺省值是在初始化列表出生效的。

在这里插入图片描述

6.成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

我们看下面的代码:

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

在这里插入图片描述

由于在类中_a2是声明在_a1之前,所以在初始化列表处_a2(_a1)语句会被先执行,此时_a1还是一个随机值,所以最终_a2的值为一个随机值。

二、隐式类型转换

1.概念

隐式类型转换是指当两个不同类型的变量之间进行运算的时候,编译器会自动将其中一个变量的类型转换为另一个变量的类型,比如我们下面的代码:

int main()
{
    
    
	int i = 0;
	double d = i;
	const double rd = i;
	const int& j = 1;
	return 0;
}

对于上面的代码,我们将一个整型的i赋值给双精度浮点型的d,此时其实不是直接把i赋值给d,编译器会先根据i的值创建一个浮点型的临时变量,再把临时变量赋值给d;

对于rd来说也是如此,不同的是rd是引用类型,而引用和指针我们不要考虑权限的放大,缩小和平移的问题,我们用引用类型的变量rd去引用d生成的临时变量需要用const修饰,因为临时变量具有常性;

对于j来说也是如此,由于数字1只存于指令中,在内存中并不占用空间,所以当我们对其进行引用的时候,1会先生成一个临时变量,然后再对临时变量进行引用,又因为临时变量具有常性,所以我们也需要加const进行修饰。

2.构造函数的类型转换

在C++98中,构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用,比如下面的这样:

class Date
{
    
    
public:
	Date(int year)
		:_year(year)
	{
    
    
		cout << "Date(int year)构造" << endl;
	}

	Date(const Date& d)
	{
    
    
		_year = d._year;
		_month = d._month;
		_day = d._day;
		cout << "Date(const Date& d)拷贝构造" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
    
    
	// 构造
	Date d1(2023);
	// 隐式类型的转换 构造+拷贝构造
	Date d2 = 2023;
	// 拷贝构造
	Date d3(d1);
	// 构造
	const Date& d4 = 2023;
	return 0;
}

在这里插入图片描述

在这里插入图片描述

对于具有单参数的构造函数的Date类,我们不仅可以使用构造和拷贝构造的方式进行实例化对象,还可以通过直接赋值一个整形来进行初始化。这是隐式类型转换的结果,对于d2来说,2023和d2的类型是不同的,所以编译器会进行类型转换,即先使用2023来构造一个临时的Date类型的对象,然后后这个临时的对象来对d2进行拷贝构造,这是构造+拷贝构造的结果。

但是现在编译器会对这种情况进行优化,不再创建临时对象,而是直接使用2023来构造d2,所以我们看到的是d2没有调用拷贝构造函数,对于比较老的编译器而言,并没有做出优化,而是构造+拷贝构造。

对于d4而言,d4是Date对象的引用,所以编译器会先用2023来构造一个Date类型的临时对象,然后d4才对这个临时对象进行引用,所以只调用了一次构造函数,又因为临时变量具有常性,所以我们需要加const进行修饰。

【注意】

单参数构造函数不是指构造函数只有一个参数,而是指只需要传递一个参数的构造函数,比如全缺省或者半缺省的构造函数同样也是可以的。

C++11对单参数构造进行了扩展,支持了多参数的构造函数,只是传递的多个参数需要用大括号括起来,比如下面的代码:

class Date
{
    
    
public:
	Date(int year, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{
    
    
		cout << "Date(int year)构造" << endl;
	}

	Date(const Date& d)
	{
    
    
		_year = d._year;
		_month = d._month;
		_day = d._day;
		cout << "Date(const Date& d)拷贝构造" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
    
    
	// 构造
	Date d1(2023, 1, 1);
	// 隐式类型的转换 构造+拷贝构造
	Date d2 = {
    
     2023,1,1 };
	// 拷贝构造
	Date d3(d1);
	// 构造
	const Date& d4 = {
    
     2023,1,1 };
	return 0;
}

在这里插入图片描述

3.explict关键字

explicit关键字用于修饰构造函数,作用是禁止构造函数的隐式转换,比如一下代码:

class Date
{
    
    
public:
	explicit Date(int year, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{
    
    
		cout << "Date(int year)构造" << endl;
	}

	Date(const Date& d)
	{
    
    
		_year = d._year;
		_month = d._month;
		_day = d._day;
		cout << "Date(const Date& d)拷贝构造" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
    
    
	// 构造
	Date d1(2023, 1, 1);
	// 隐式类型的转换 构造+拷贝构造
	Date d2 = {
    
     2023,1,1 };
	// 拷贝构造
	Date d3(d1);
	// 构造
	const Date& d4 = {
    
     2023,1,1 };
	return 0;
}

在这里插入图片描述

4.类型转换的意义

构造函数的类型转换在某些特定的场景下有着很大的意义,比如我们要在一个数组的尾部插入一个字符串:

int main()
{
    
    
	string s("hello world");
	push_back(s);

	push_back("hello world");
}

如上,我们有了隐式类型转换之后,就不要先创建一个string对象然后再进行插入,而是只需要以字符串做参数即可,编译器会对其进行隐式类型的转换,称为一个string类的对象。

三、Static成员

1.概念

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化

我们以一个题目来介绍静态成员的相关知识点

题目:实现一个类,计算程序中创建出了多少个类对象

我们知道,创建对象就一定会调用构造函数或者拷贝构造函数,所以我们只需要定义一个全局变量,然后在构造函数和拷贝构造函数类进行让其自增即可,代码如下:

#include <iostream>
using namespace std;

int Count = 0;
class A
{
    
    
public:
	A(int a = 0)
	{
    
    
		_a = a;
		++Count;
	}
	A(const A& a)
	{
    
    
		_a = a._a;
		++Count;
	}
private:
	int _a;
};

int main()
{
    
    
	A aa1;
	A aa2(aa1);
	A aa3 = 1;
	cout << Count << endl;
}

在这里插入图片描述

虽然使用全局变量的方法可以十分简便的达到题目的要求,但是我们不建议使用全局变量,因为全局变量可以被任何人修改,十分不安全,所以我们需要使用另外一种比较安全的方法–静态成员变量。

2.static成员变量

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;其特征如下:

1.静态成员所有类对象所共享,不属于某个具体的对象,存放在静态区

2.静态成员变量必须在类外定义定义时不添加static关键字,类中只是声明

3.类静态成员即可用 类名::静态成员 或者 对象.静态成员来访问

1.静态成员存放在静态区(数据段),所以不在对象里面,所以它不属于某个对象,而是所有的对象共享。

class A
{
    
    
public:
	A(int a = 0)
		:_a(a)
	{
    
    }

	int _a;
	static int _n;
};

int A::_n = 0;

int main()
{
    
    
	A* a = nullptr;
	cout << a->_n << endl;
	cout << A::_n << endl;
}

在这里插入图片描述

我们可以看到,当我们可以直接通过类名+域作用限定符或者通过一个空指针对对象进行访问,说明_n并不存在对面里面。

2.类的静态成员在类中只是声明,必须在类外进行定义且定义的时候需要指定类域,其不在初始化列表进行定义初始化,因为创建对象的时候并不会改变它的值。

在这里插入图片描述

3.静态成员变量的访问受类域和访问限定符的限制:

class A
{
    
    
public:
	A(int a = 0)
		:_a(a)
	{
    
    }

private:
	int _a;
	static int _n;
};

int A::_n = 0;

int main()
{
    
    
	A* a = nullptr;
	cout << a->_n << endl;
	cout << _n << endl;
}

在这里插入图片描述

【注意】

静态成员变量在访问时和普通的成员变量区别不大,同样受类域和访问限定符的约束,只是由于静态成员变量存放在静态区中,被所有的对象所共享,所有我们可以通过指定域的方式对其进行访问,此外,静态成员变量在定义的时候只受类域的限制,而没有受访问限定符的限制。

那么我们就可以使用静态成员变量来解决上面的题目了,代码如下:

class A
{
    
    
public:
	A(int a = 0)
	{
    
    
		_a = a;
		++_count;
	}
	A(const A& a)
	{
    
    
		_a = a._a;
		++_count;
	}
private:
	int _a;
	static int _count;
};

int A::_count = 0;

int main()
{
    
    
	A aa1;
	A aa2(aa1);
	A aa3 = 1;
	cout <<A:: _count << endl;
}

在这里插入图片描述

我们可以看到,由于我们把成员变量设置成了私有,在类的外面无法访问私有变量,我们可以把私有转变为共有,或者在类中提供获取成员变量的共有函数,但是这样又破坏了类的封装性,当初设计类的时候就行不想在类的外部随便更改和读取类中的成员,那么我们该怎么解决这个问题呢?针对这个问题,C++设计出了静态成员函数。

3.static成员函数

用static修饰的成员函数,称之为静态成员函数。其特性如下:

1.静态成员变量一定要在类外进行初始化

2.静态成员函数没有隐藏的this指针,不能访问任何非静态成员

3.静态成员也是类的成员,受public、protected、private 访问限定符的限制

由于静态成员函数没有隐藏的this指针,所以我们在调用的时候就不需要传递对象的地址,即我们可以通过类名+域作用限定符直接调用,而不需要创建对象,但是相应的,没有了this指针我们也无法调用非静态成员函数和成员变量,因为非静态成员变量需要实例化对象来开辟空间,非静态成员函数的调用则需要传递对象的地址。但是虽然静态成员函数不可以调用非静态成员,但是非静态成员函数可以调用静态成员,调用静态成员时编译器不需要传递对象的地址。

所以上面的题目正确的代码如下:

class A
{
    
    
public:
	A(int a = 0)
	{
    
    
		_a = a;
		++_count;
	}
	A(const A& a)
	{
    
    
		_a = a._a;
		++_count;
	}

	static int Getcount()
	{
    
    
		return _count;
	}
private:
	int _a;
	static int _count;
};

int A::_count = 0;

int main()
{
    
    
	A aa1;
	A aa2(aa1);
	A aa3 = 1;
	cout <<A:: Getcount() << endl;
}

四、友元

我们去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。

流插入和流提取的重载我已经在类和对象的中篇进行了详细的说明,大家有兴趣可以去看看,这里我们就不再赘述。

1.友元函数

友元函数可以直接访问类的私有成员,它是定义在类外部普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字

class Date
{
    
    
private:
	int _year;
	int _month;
	int _day;

public:
	//友元声明 声明可以在类中的任意位置
	friend ostream& operator<<(ostream& out, const Date& d);
	friend istream& operator >>(istream& in, Date& d);
	Date(int year = 1, int month = 1, int day = 1)
	{
    
    
		_year = year;
		_month = month;
		_day = day;

		//检查日期是否合法
		if (!(year >= 1
			&& (month >= 1 && month <= 12)
			&& (day >= 1 && day <= GetMonthDay(year, month))))
		{
    
    
			cout << "非法日期" << endl;
		}
	}
}

// operator<<(cout, d1) cout<<d1
// ostream& operator<<(ostream& out, const Date& d);
// 流插入
inline ostream& operator<<(ostream& out, const Date& d)
{
    
    
	out << d._year << "-" << d._month << "-" << d._day << endl;
	//连续打印,返回out;
	return out;
}

// cin >> d1  operator(cin, d1)
// 流提取

inline istream& operator >>(istream& in, Date& d)
{
    
    
	in >> d._year >> d._month >> d._day;
	//连续输入
	return in;
}

【总结】

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

2.友元函数不能用const修饰

3.友元函数可以在类定义的任何地方声明,不受类访问限定符限制

4.一个函数可以是多个类的友元函数

5.友元函数的调用与普通函数的调用原理相同

2.友元类

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

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

在这里插入图片描述

友元类的特点:

1.友元关系是单向的,不具有交换性比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行

2.友元关系不能传递,如果C是B的友元, B是A的友元,则不能说明C时A的友元

3.友元关系不能继承

五、内部类

概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限

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());

	return 0;
}

在这里插入图片描述

内部类的特性:

1.内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元

2.内部类可以定义在外部类的public、protected、private都是可以的

3.注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名

4.sizeof(外部类)=外部类,和内部类没有任何关系

六、匿名对象

在C++中,除了用类名+对象创建对象外,我们还可以直接使用类名来创建匿名对象,匿名对象和正常对象一样,在创建时自动调用构造函数,在销毁时调用析构函数,但是匿名对象的生命周期只有在定义的那一行,下一行就会立马销毁。

class A
{
    
    
public:
	A(int a = 0)
		:_a(a)
	{
    
    
		cout << "A(int a = 0)构造" << endl;
	}
	~A()
	{
    
    
		cout << "~A()析构" << endl;
	}

private:
	int _a;
};

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

在这里插入图片描述

匿名对象的应用有如下场景,对于其他场景我们遇到了再说:

class Solution
{
    
    
public:
	int Sum_solution(int n)
	{
    
    
		//...
		return n;
	}
};

int main()
{
    
    
	//Solution so;
	//so.Sum_solution(0);

	Solution().Sum_solution(0);
	return 0;
}

在上面的代码中我们使用Solution().Sum_solution(0)一行代替了Solution so;so.Sum_solution(0);两行。

七、拷贝对象时的一些编译器优化

在传参和传值返回的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在某些场景写是十分有用的。

class A
{
    
    
public:
	A(int a = 0)
		:_a(a)
	{
    
    
		cout << "A(int a)" << endl;
	}
	A(const A& aa)
		:_a(aa._a)
	{
    
    
		cout << "A(const A& aa)" << endl;
	}
		A& operator=(const A& aa)
	{
    
    
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
    
    
			_a = aa._a;
		}
		return *this;
	}
	~A()
	{
    
    
		cout << "~A()" << endl;
	}
private:
	int _a;
};

//传值传参
void f1(A aa)
{
    
    }

//传值返回
A f2()
{
    
    
	//A aa;
	//return aa;
    return  A();
}
int main()
{
    
    
	// 传值传参
	A aa1;
	f1(aa1);
	cout << endl;
	// 传值返回
	f2();
	cout << endl;
	// 隐式类型,连续构造+拷贝构造->优化为直接构造
	f1(1);
	// 一个表达式中,连续构造+拷贝构造->优化为一个构造
	f1(A(2));
	cout << endl;
	// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
	A aa2 = f2();
	cout << endl;
	// 一个表达式中,连续拷贝构造+赋值重载->无法优化
	aa1 = f2();
	cout << endl;
	return 0;
}

优化场景1:传参隐式类型转换–构造+拷贝构造 ->直接构造

在这里插入图片描述

我们调用f1函数,并使用1作为参数,由于1和f1的形参不同,所以会发生隐式类型的转换,即编译器会先使用1去构造A类型的临时变量,然后用这个临时变量去拷贝构造aa,所以这里本来应该是构造+拷贝构造,但是编译器将其优化直接使用1去构造aa

优化场景2:匿名对象–构造+拷贝构造->直接构造

在这里插入图片描述

原本是用2来构造一个匿名对象,再用匿名对象来拷贝构造aa,经过编译器优化后直接使用2去构造aa

优化场景3:传值返回–构造+拷贝构造+拷贝构造->直接构造

在这里插入图片描述

f2返回的是局部的匿名对象,所以编译器会先用匿名对象去拷贝构造一个临时对象,然后再用临时对象来拷贝构造aa2,而经过编译器的优化直接使用无参构造aa2,即构造+拷贝构造+拷贝构造优化为直接构造。

【注意】

编译器只能对一句表达式中的某些操作进行优化,因为编译器必须保证程序的正确性,所以不能将两句表达式优化成一句,避免程序发生错误。

八、再次理解类和对象

现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:

1.用户先要对现实中洗衣机实体进行抽象—即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程

2.经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、Java、Python等)将洗衣机用类来进行描述,并输入到计算机中

3.经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能洗衣机是什么东西。

4.用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。

在类和对象阶段,大家一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象

在这里插入图片描述

九、总结

初始化列表的特性(重要)

1.初始化列表是每个成员变量定义和初始化的地方,所以每个成员变量(内置类型和自定义类型),无论我们是否显示在初始化列表出写,都一定会走初始列表,并且初始化操作只能进行一次,即每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

2.如果我们在初始化列表显示写了编译器就会用显示写的来初始化,如果我们没有在初始化列表显示的写,那么对于内置类型,有缺省值用缺省值,没有编译器就会使用随机值来初始化,对于自定义类型,编译器会调用自定义类型的默认构造函数来初始化,如果没有默认构造编译器就会报错

3.类中包含以下成员(引用,const,没有默认构造的自定义成员),必须放在初始化列表位置进行初始化

4.尽量使用初始化列表进行初始化,因为无论我们是否在初始化列表显示初始化,类的成员函数在初始化的时候都会先使用初始化列表进行初始化

5.在C++11中对于内置类型打了一个补丁–内置类型成员变量可以在声明的时候给一个缺省值,其可以在初始化列表起作用

6.成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

猜你喜欢

转载自blog.csdn.net/qq_67582098/article/details/128707861