【C++学习】类与对象(下)

作者:一只大喵咪1201
专栏:《C++学习》
格言:你只管努力,剩下的交给时间!
【C++学习】类与对象(下)_第1张图片

类与对象(下)

  • const成员函数
  • 流插入(<<)和流提取(>>)运算符重载
  • 取地址运算符(&)和const取地址运算符重载
  • 再谈构造函数
    • 初始化列表
    • explicit关键字
  • static成员
  • 友元
    • 友元函数
    • 友元类
  • 内部类
  • 匿名对象
  • 拷贝对象时的一些编译器优化
  • 再次理解类和对象
  • 总结

在上篇文章【C++学习】类与对象(中)里,本喵详细介绍了六大默认成员函数中的四个,构造函数,析构函数,拷贝构造函数,赋值运算符重载,接下来本喵继续介绍剩下的俩个不是很重要的默认成员函数,已经类与对象其他重要内容。

const成员函数

我们知道,const修饰的变量具有常量属性,是不可以被修改的,在C++中,const还可以修饰函数,那么此时的const的作用是什么呢?

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

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}


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

void Test1()
{
	Date d1(2020, 10, 17);
	d1.Print();
}

【C++学习】类与对象(下)_第2张图片
仍然是以日期类为例,上面的代码成功打印出了当前的日期,下面本喵对其稍加修改:

【C++学习】类与对象(下)_第3张图片
这里用const修饰日期类对象d2,然后用d2调用成员函数Print的时候,发现不能调用了,这是什么原因?

【C++学习】类与对象(下)_第4张图片

  • 没有用const修饰的对象d1,在调用成员函数的时候,传递的隐藏指针类型是Date* const this类型,与成员函数Print的隐藏参数类型Date* const this一样,所以没有问题。
  • 用const修饰的对象d2,在调用成员函数的时候,传递的隐藏指针类型是const Date* const this类型,与成员函数Print的隐藏参数类型Date* const this不一样,相当于在放大权限,这是不被允许的,所以会报错。

那该怎么解决这个问题呢?

【C++学习】类与对象(下)_第5张图片
在定义成员函数的时候加一个const就解决了这个问题。

【C++学习】类与对象(下)_第6张图片
通过调试我们可以看到,在d2调用Print成员函数的时候,调用的是类中用const修饰的那个Print成员函数。

【C++学习】类与对象(下)_第7张图片

  • 用const修饰成员函数,此时const的作用就是将this指针用const修饰
  • const只修饰this指针,如果成员函数有其他参数是不会被const修饰的。

注意:

  • const对象不可以调用非const成员函数,因为const对象的this指针是具有常属性的,非const成员函数形参的隐藏this指针是没有常属性的,会导致权限放大的问题。
  • 非const对象可以调用const成员函数,因为权限可以被缩小,但是不能被放大。
  • const成员函数内不可以调用其它的非const成员函数,因为const成员函数的this指针是被const修饰的,在调用其他非cosnst成员函数的时候,还是使用的这个指针,但是非const成员函数的this指针没有被const修饰,所以也会导致权限放大的问题。
  • 非const成员函数内可以调用其它的const成员函数,同样的道理,权限可以被缩小,但是不能被放大。

总的来说,凡是不改变成员变量的成员函数,都应该用const修饰。

流插入(<<)和流提取(>>)运算符重载

在C++中,打印的时候需要使用cout库函数,而且我们知道,该函数可以自动识别变量的类型,通过前面的学习,我们可以猜测出来,自动识别的原理是函数重载,不同的内置类型就调用不同的成员函数,那么自定义类型cout可以识别出来吗?

继续来看上面的日期类,其中打印函数是一个成员函数:

【C++学习】类与对象(下)_第8张图片
其中的变量都是内置类型,那么可不可以用cout打印自定义类型呢?此时它还能自动识别吗?

【C++学习】类与对象(下)_第9张图片
可以看到,此时就直接报错了,因为cout此时不能识别d1的类型了.

【C++学习】类与对象(下)_第10张图片

上图是C++官方的库,拿cout函数举例,cout其实是一个对象,它是按照类ostream创建的一个对象,而我们流提取运算符是ostream类中的成员函数,本喵拿代码给大家展示:

【C++学习】类与对象(下)_第11张图片

上图中,类名是ostream,而流提取运算符是C语言中左移操作符<<的重载,

所以我们要想使用<<来打印出自定义类型,就得将运算符<<也重载。

【C++学习】类与对象(下)_第12张图片
就像上图中的样子,但是这样还是存在一个问题,<<运算符重载是类Date中定义的,只能通过Date创建的对象d1来调用它,也就是说,运算符重载函数<<的第一个操作数只能是d1。

但是此时第一个操作数是cout,调用<<的时候传递就是不是d1的this指针了,所以这样还是不行。

【C++学习】类与对象(下)_第13张图片
我们继续修改,如上图,将类对象d1当作第一操作数,此时调用<<重载函数的时候,this指针传递就是d1对象的地址,所以<<重载函数的参数要写一个ostream类型的对象,在重载函数内,将内置成员变量输出到控制台上。

此时是实现了我们的目的,但是这个调用怎么看怎么奇怪,和我们日常见的不符合,为了和我们日常见的一样,我们只能将<<重载定义到类外面:

【C++学习】类与对象(下)_第14张图片
可以看到,此时使用cout就和我们的习惯一样,并且还能够自动识别类型,因为此时将<<运算符重载定义成一个全局函数,该函数有俩个参数,第一个参数对应第一个操作数的类型,是cout,所以类型就是ostream&,第一个参数对应的是第二个操作苏的类型,是d1,所以类型就是Date&。

在域外访问类中的私有变量,这里采用定义几个取值的类成员函数来访问,就像上图中获取年月日的函数Get_year,Get_month,Get_day。

至于为什么将<<运算符重载函数的返回类型写成ostream&呢?这是为了方便输出多个自定义变量:

【C++学习】类与对象(下)_第15张图片
这样的表达式,执行顺序是从左向右的,当执行完cout<

流插入运算符>>也是相同的道理,有兴趣的小伙伴可以自己试试,需要注意的就是,为了和我们的使用习惯一致,>>运算符重载函数也必须定义成全局函数,不能定义成类中的函数。

取地址运算符(&)和const取地址运算符重载

&运算符重载同样也是一个默认成员函数,但是它很少使用到。

class Date
{
public:
	Date(int year = 1970, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//取地址运算符重载
	Date* operator&()
	{
		return this;
	}

	//const取地址运算重载
	const Date* operator&()const
	{
		return this;
	}
private:
	int _year;
	int _month;
	int _day;
};

上面程序的内容就是取地址运算重载和const取地址运算符重载。

【C++学习】类与对象(下)_第16张图片

  • 类对象d1没有别const修饰,所以取地址的时候就调用取地址运算符重载函数
  • 类对象d2被const修饰了,所以取地址的时候就调用const取地址运算符重载函数

所谓const取地址和取地址,仅仅在于类对象是否被const修饰,被修饰了this指针也需要被修饰,运算符中函数也需要被const修饰,并且返回类型也是被const修饰的指针类型。

但是这俩个运算符重载很少需要我们自己去实现,因为编译器自动生成的就完全可以满足需求,所以不需要我们去写。

但是也有应用场景:

【C++学习】类与对象(下)_第17张图片
此时,俩个取地址运算符重载函数返回的地址是空地址,因为将取地址运算符进行了显式定义,编译器就不回自动生成了,所以这样写时,意味着该对象的地址是不想被别人获取的。

但是一般都不这样使用,所以编译器自动生成的完全能够满足我们的需求。

再谈构造函数

初始化列表

通过前面的学习我们知道,构造函数的作用就是将成员变量初始化,其实从严格意义上来说,不能叫做初始化,只能叫做是赋值。

【C++学习】类与对象(下)_第18张图片
我们之前一直写的日期类是这个样子的,其实类中只有对成员变量进行声明和赋值的地方,但是没有定义的地方,不信你来看:

【C++学习】类与对象(下)_第19张图片
上图中,仅仅是在声明中将成员变量_year用const修饰,就报错该成员变量不可以被修改,因为它具有常属性。

  • 被const修饰的成员变量和引用变量一样,只有一次初始化机会,那就是在定义的时候进行初始化

这也说明,原本的构造函数的作用就是在赋值,而没有给成员变量进行定义,那么成员变量的定义是在哪里进行的呢?

成员变量的定义是在初始化列表中完成的!!!

【C++学习】类与对象(下)_第20张图片
只需要像上图中那样,用初始化列表来给const修饰的成员变量_year进行赋值即可。此时就是在定义的时候进行初始化。

  • 初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{}

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

初始化列表必须按照上面的格式,这是C++标准规定的。

初始化列表在对象创建的时候是一定会有的,和构造函数一样,即使没有显式定义出来,编译器也会有默认生成的。

【C++学习】类与对象(下)_第21张图片
上图中并没有进行显式定义,打印出来的结果是随机数。

  • 构造函数显式定义了,是默认构造函数,没有任何参数
  • 初始化列表没有显式定义,此时编译器自动生成了初始化列表,将这些成员变量定义,并且初始化为随机数。

注意:

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
  • 引用成员变量:
    引用变量是另一个变量的别名,而且一经定义初始化以后就不可以被修改,因为构造函数的功能是给成员函数赋值,所以构造函数中不能操作引用变量,否则就会报错,引用成员变量只能在初始化列表中进行初始化。

  • const修饰的成员变量:
    const修饰的成员变量具有常量属性,是无法在构造函数中赋值的,所以也必须在初始化列表中进行初始化。

  • 自定义成员变量(且该类没有默认构造函数):
    【C++学习】类与对象(下)_第22张图片
    继续拿出我们的栈,上图中的栈是没有默认构造函数的,此时的构造函数需要传参。

【C++学习】类与对象(下)_第23张图片
我们用俩个栈实现一个队列的时候,其中俩个栈自定义类型,并且没有默认的构造函数。
【C++学习】类与对象(下)_第24张图片

运行的时候编译器就报错了,提示栈没有默认构造函数。

因为在类MyQueue中定义Stack对象的时候,编译器自动生成的初始化列表并不会给Stack对象传参,Stack对象也没有默认函数。

此时,自定义类型Stack只能在初始化列表中进行初始化。

【C++学习】类与对象(下)_第25张图片
上图中,将俩个栈在初始化列表中进行初始化,括号中的初始化值就相当于在给栈的构造函数传参。

但是当自定义类型有默认构造函数的时候,就不用显式写出初始化列表了,编译器自动生成的初始化列表在定义自定义类型栈的时候会调用栈的默认构造函数。

【C++学习】类与对象(下)_第26张图片
上图中没有显式初始化列表,但是可以成功定义并初始化的,因为自定义类型栈有默认构造函数,在创建队列对象的时候,编译器自动生成的初始化列表会定义栈对象,并且调用它的默认构造函数。

此时我们就可以总结一下了:

  • 没有显式初始化列表时:
    内置类型初始化时,有缺省值就用缺省值,没有就初始化为随机数
    自定义类型,调用它的默认构造函数,如果没有就报错
  • 有显式初始化列表时:
    使用初始化列表中的定义和初始化
  1. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化,而且可以和构造函数混着用。

【C++学习】类与对象(下)_第27张图片
我们的栈类就可以定义为这样。

  • 只需赋值的成员变量在初始化列表中定义并且初始化
  • 需要开辟动态空间的指针变量,在初始化列表中由编译器自动定义,在构造函数中进行赋值和正确性检测。
  1. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
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();
	return 0;
}

上面代码的运行结果是什么?是俩个1嘛?
【C++学习】类与对象(下)_第28张图片
运行结果是1和一个随机数,很意外吧?

这是因为,在初始化列表中定义成员变量的时候,是按照成员变量声明的顺序来的,和成员变量在初始化列表中的顺序无关。

  • 上面类A中,成员变量的声明顺序是先_a2,再_a1
  • 初始化列表中成员变量的顺序的是先_a1(a),再_a2(a1)
  • 在创建对象aa的时候,先定义成员变量_a2,再定义成员变量_a1,所以用_a1初始化_a2的时候,_a1还是一个未定义的随机数。
    定义_a1的时候,是用的传过来的值1初始化的,并不影响,所以最后的结果就是一个1和一个随机数。

explicit关键字

构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。

仍然是日期类:
【C++学习】类与对象(下)_第29张图片
使用初始化列表来初始化日期对象。

【C++学习】类与对象(下)_第30张图片
看上图中,创建了3个日期类对象,其中前俩个的初始化方式我们都见过,一个是给默认构造函数只传一个产生,其余俩个用缺省值来初始化。第二个是用赋值运算重载,将对象d1赋值给d2的初始化,其实这也的实质是利用拷贝构造函数来初始化的,大家可以自己调试去看看。

但是第三个是什么意思?怎么直接将一个int类型的数据赋值给自定义类型了呢?

  • 这就是隐式类型转换,将int类型的数据转换为了自定义类型Date,也是通过构造函数来实现的。
  • 可以隐式类型转换的构造函数,其形参只能有一个,但是不包括缺省参数,换言之,其形参可以是全缺省的,也可以是半缺省的(只有一个形参不是缺省值),亦或是只有一个形参,并且不是缺省值。

【C++学习】类与对象(下)_第31张图片
这是它的转换过程。编译器会对这个过程进行优化,后面我们再说怎么优化的。

说是隐式类型转换,也就是将一个int类型的值给了构造函数来初始化对象。

【C++学习】类与对象(下)_第32张图片

上图中,将int类型的值2024转换程Date&就不行,编译器就会报错,这个错误应该很熟悉。

引用和指针都回涉及到一个权限问题。

【C++学习】类与对象(下)_第33张图片
同样的,在进行类型转换的时候,需要将整型先转换成一个Date类型变量,并且存放在一个临时变量中。

  • 转换过程中产生的临时变量具有常性,也就是相当于用const修饰了,这样一个变量赋值给Date&类型的d4就相当于放大了权限,所以不被允许
  • 不是引用类型的转换就不存在这个问题,因为没有权限问题

所以,只要用const修饰一下对象d4就可以了。

【C++学习】类与对象(下)_第34张图片
使用const修饰d4以后就没有权限放大的问题了,就可以成功转换类型,又由于调用成员函数Print的时候,此时的指针是const Date* const this,所以成员函数也得用const修饰才行。

隐式类型的转换只支持构造函数只有一个形参的时候(不包括缺省值),但是想一次性转换多个数据怎么办呢?

【C++学习】类与对象(下)_第35张图片
可以使用大括号将多个数据括起来,每个数据直接用逗号隔开。

一直都是本喵告诉大家隐式类型转换是构造函数完成的,那么到底是不是它完成的呢?

【C++学习】类与对象(下)_第36张图片

  • 用关键字explicit修饰构造函数,如上图中的红色框
  • 再运行绿色框中的隐式类型转换,发现报错了

此时就不让进行隐式类型转换了,我只对构造函数做了处理,所以说,隐式类型转换是构造函数完成的。

static成员

我们知道,static修饰的变量是存放在静态区的,它的作用域不回发生变化,但是生命周期发生了变化,只有在程序结束的时候才会结束。

在类中成员也是可以被static修饰的。

  • 声明为static的类成员称为类的静态成员
  • 用static修饰的成员变量,称之为静态成员变量;
  • 用static修饰的成员函数,称之为静态成员函数。
  • 静态成员变量一定要在类外进行初始化
class A
{
public:
	//构造函数
	A()
	{
		_count++;
	}
	//拷贝构造函数
	A(A& a)
	{
		_count++;
	}
	//获取计数值函数
	static int GetCount()
	{
		return _count;
	}
private:
	static int _count;
};
//静态成员变量定义初始化
int A::_count = 0;

int main()
{
	A a1, a2;
	A a3(a1);
	cout << A::GetCount() << endl;
	return 0;
}

这样一段代码,是为了统计创建了多少个类对象的。
【C++学习】类与对象(下)_第37张图片
可以看到,结果显式一共创建了3个类对象,那么该函数的原理是什么呢?

【C++学习】类与对象(下)_第38张图片
成员变量_count是被static修饰的静态成员变量,它是存放在静态区的,但是属于这个类。
无论创建多少个类对象,静态成员变量只有这一个,所有的类对象是共享这一个静态成员变量的。

  • 每创建一个类对象,就回调用一次默认构造函数或者拷贝构造函数,在这俩个函数中,对静态成员变量_cout加1
  • 由于是共享的,所以结果就回累加,最终的值就创建对象的个数。

【C++学习】类与对象(下)_第39张图片
静态成员变量的定义和初始化是在类外进行的,而且是必须在类外进行。

  • 定义时不用写关键字static,因为在类中声明的时候已经表明这是一个静态成员变量了。
  • 定义时要写类名加::,否则编译器会报错,因为静态成员变量仍然是属于这个类的。

【C++学习】类与对象(下)_第40张图片
用static修饰的成员函数叫做静态成员函数,它的存储位置仍然是在公共代码区,和非静态成员一样。但是由于被static修饰,它又有新的特性。

静态成员函数没有this指针。
我们知道,在用类对象调用类中的成员函数以及访问成员变量的时候是通过this指针实现的,this指针中的地址就是该对象的地址。

但是静态成员函数没有了this指针,相比普通成员函数,注定它会有一些功能上的缺失。

静态成员函数只能方法静态成员,包括静态成员变量和静态成员函数,非静态成员是不能访问的。

【C++学习】类与对象(下)_第41张图片
上图中,成员函数中只有_year是静态成员变量,其他俩个都是非静态的。打印函数也是静态成员函数,要打印出这三个成员变量,但是编译器报错非静态成员的非法访问。这就说明,静态成员函数是不能够方法非静态成员的,因为它没有this指针,就无法准确定位到是哪个类对象。

【C++学习】类与对象(下)_第42张图片
只打印静态成员变量是可以的,因为静态成员函数只能访问静态成员。

当然,静态成员的访问还可以通过其他方式来访问:

【C++学习】类与对象(下)_第43张图片
就拿静态成员函数来说,访问它的方式就有俩种。

  1. 类对象.静态成员函数(使用点操作符来访问)
  2. 类名::成员函数(使用域作用限制符来访问)

但是,静态成员仍然收到public,private,protected等限制符的限制。

【C++学习】类与对象(下)_第44张图片
可以看到,无论使用这俩种的哪种,即使静态成员变量存在静态区,但是因为它是私有的,所以在类域外是无法访问的。

友元

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

友元函数

在流插入和流提取运算符重载的讲解中,我们写了流插入运算符重载函数,但是为了符合使用习惯,使可读性高,我们将这个函数定义成了全局函数,但是在打印对象d1的时候,由于类中对成员变量的封装,无法直接访问,所以就写了三个成员函数来获取成员变量的值。如下图:

【C++学习】类与对象(下)_第45张图片
现在我们有一个新的方法来访问被封装的成员变量,就是友元。

【C++学习】类与对象(下)_第46张图片
将流插入运算符重载函数的函数声明用关键字friend修饰,然后放在类中,可以在类中的任何地方。

此时该函数就可以访问类中的私有成员变量了,因为它是友元函数,编译器认为该函数是这个类的朋友。

说明:

  1. 友元函数可访问类的私有和保护成员,但不是类的成员函数
  2. 友元函数不能用const修饰
  3. 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  4. 一个函数可以是多个类的友元函数
  5. 友元函数的调用与普通函数的调用原理相同

友元类

友元类和友元函数类似,一个类的友元类可以方法它内部的任何成员。

【C++学习】类与对象(下)_第47张图片
创建一个时间类,在类中将日期类声明为友元类,此时日期类就可以访问时间类中的任何成员。

【C++学习】类与对象(下)_第48张图片

在日期类中,声明一下时间类,此时就可以访问时间类中的任何成员了,因为时间类中已经认为日期类是它的友元类了。

【C++学习】类与对象(下)_第49张图片
此时就通过日期类中的成员函数就可以将日期类和时间类中的成员变量都打印出来。

说明:

  1. 友元关系是单向的,不具有交换性:
    比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
  2. 友元关系不能传递:
    如果C是B的友元, B是A的友元,则不能说明C时A的友元。
  3. 友元关系不能继承,在继承位置再给大家详细介绍。

内部类

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

【C++学习】类与对象(下)_第50张图片
上面程序中,将日期类在时间类中定义,此时日期类就是时间类的内部类。可以通过内部类的成员函数来访问外部类的私有变量,就和上面的友元类一样,此时内部类是外部类的友元类,即日期类是时间类的友元类。
【C++学习】类与对象(下)_第51张图片
创建内部类的时候,要使用 外部类::内部类 对象名的方式类创建内部类,并且进行初始化,外部类的创建直接使用 外部类::对象名的方式创建。

可以通过内部类的成员函数访问到外部类,如上图中,成功打印出了日期加时间。

说明:

  1. 内部类天生就是外部类的友元类,参见友元类的定义,内部类可以通过内部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
  2. 内部类可以定义在外部类的public、protected、private都是可以的,也就是在任意地方定义都可以。
  3. 内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  4. sizeof(外部类)=外部类,和内部类没有任何关系。
    这里就可以看出来,内部类和外部类其实是俩个独立的类,它们只是存在内部类天生就是外部类友元的关系,其他都是相互独立的。

匿名对象

匿名对象,顾名思义就是没有名字的对象,我们之前一直创建的对象都是有对象名的。

【C++学习】类与对象(下)_第52张图片
上图中,创建一个有名的日期对象,在构造函数和析构函数中分别加一句打印语句来证明这个对象确实存在过并且销毁。
【C++学习】类与对象(下)_第53张图片
可以看到,对象d1确实创建了,并且在程序结束的时候被销毁了。

那么匿名对象呢?都没有名字,怎么能证明它存在呢?

【C++学习】类与对象(下)_第54张图片

上图中,创建了一个匿名对象和一个有名对象。

【C++学习】类与对象(下)_第55张图片
通过调试我们可以看到,此时执行到了要创建有名对象d1的时候,在这之前,匿名对象是已经创建过的,而且控制台也对应着输出了构造函数和析构函数,说明此时已经执行过一次构造函数和一次析构函数了。

这俩个函数的执行只能是匿名对象创建和销毁时候执行的。

  • 匿名对象的声明周期就在当前语句,它在执行当前行的时候被创建,在执行下一行的时候被销毁。

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

在进行类对象的创建,赋值等操作时候,会自动调用构造函数,拷贝构造函数,如果连续调用构造函数和拷贝函数,系统的开销也非常的大,此时编译器就会自动做一些优化。

【C++学习】类与对象(下)_第56张图片
创建一个类如上图中,类中显式定义了构造函数,拷贝构造函数,赋值运算符重载,以及析构函数,并且在每个函数中都加了一个打印语句。

【C++学习】类与对象(下)_第57张图片
再定义俩个函数,一个是传值调用函数,一个是传值返回函数。

【C++学习】类与对象(下)_第58张图片

创建一个对象aa1,然后调用函数func1,将对象aa1传过去,右边是打印的结果,可以看到在这个过程中调用了构造函数,拷贝构造函数,析构函数,析构函数。

  • 构造函数:在创建对象aa1的时候调用(隐式类型转换是构造函数完成的)。
  • 拷贝构造函数:在给函数func1传参的时候,函数的形参需要拷贝实参的内容,所以在此时调用。
  • 第一次析构函数:在函数func1执行完毕以后,形参会被销毁,在此时调用
  • 第二次析构函数:在整个程序执行结束时,main函数中的对象aa1被销毁,此时调用。

可以看到,就这样一个功能,需要调用这么多的函数,如果这样的操作有很多的话就会有很大的开销,所以编译器对这些做了一些优化。
【C++学习】类与对象(下)_第59张图片
上图中,我们将之前的俩句才能完成的功能写成一句。

  • 调用函数func1,并且传参1
  • 函数的形参接收实参1,并且进行隐式类型转换
  • 隐式类型转换的实质是先创建一个临时变量,此时会调用一次构造函数,再将临时变量中的值拷贝给形参,此时会调用一次拷贝构造函数,然后临时变量销毁,此时会调用一次析构函数。
  • 函数执行完毕后形参被销毁,此时会调用析构函数。

原本需要调用四个函数(已经优化隐士类型转换的情况下),现在俩个就能解决,而且实现的功能是完全一致的。由于最后都有一个析构函数,所以此时就可得出结论:

  • 优化前:构造函数+拷贝构造函数+析构函数
  • 优化后:构造函数
  • 相当于三合一了

这种更像是在写法上的优化,不太能看到编译器的直接优化结果。

【C++学习】类与对象(下)_第60张图片

上图中,实现的功能合之前是一样的,只是使用了匿名对象。

  • 匿名对象在创建的时候会调用构造函数(包括隐式类型转换)
  • 在将匿名对象传参给形参,形参在接收的时候会调用拷贝构造函数
  • 之后匿名对象销毁,会调用析构函数
  • 之后函数执行结束后仍然会调用一次析构函数

此时从结果中我们可以看到,只有构造函数和析构函数,同样抛开最后一次的析构函数不看,因为无论哪种方式都会有一次析构函数。

  • 优化前:构造函数+拷贝构造函数+析构函数
  • 优化后:构造函数
  • 相当于三合一了。

但是一些比较老的编译器就不会进行优化。

再次理解类和对象

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

  1. 用户先要对现实中洗衣机实体进行抽象—即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程。
  2. 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、Java、Python等)将洗衣机用类来进行描述,并输入到计算机中。
  3. 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能知道洗衣机是什么东西。
  4. 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。

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

【C++学习】类与对象(下)_第61张图片

总结

类与对象是从C语言迈入C++的一个门槛,它的细节有很多,大部分本喵在这三篇文章中也详细的讲解过了,掌握了这些,就拿下了C++的一血。在后面的学习中也会不断用到这部分知识。

你可能感兴趣的:(C++学习,c++,学习)