草庐IT

【C++】类和对象(完结篇)

YIN_^O^ 2023-04-14 原文

文章目录

这篇文章呢,我们来再来对类和对象做一些补充,进行一个最后的首尾!

1. 再谈构造函数

那上一篇文章呢,我们学了类的6个默认成员函数,其中我们第一个学的就是构造函数

那我们先来回忆一下构造函数:

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
也就是说,构造函数其实就是帮我们对类的成员变量赋一个初值

举个栗子:

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

对于像这样的一个类来说:

虽然经过上述构造函数的调用之后,对象中的成员变量已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋值
因为初始化只能初始化一次(即在定义的时候赋初值),而构造函数体内可以对成员变量进行多次赋值

这里注意初始化(定义的时候赋初值)和赋值的区别。

那我们现在来看这样一个类:

class A
{
private:
	int _a1;
	int _a2;
};

问大家一个问题:这里面的int _a1; int _a2;

是对成员变量_a1、 _a2的声明还是定义?
这里是不是声明啊,只是声明一下A这个类里有这样两个成员变量。

那它们在哪定义呢?

🆗是不是在这个时候:

但是,这里不是对对象整体的定义嘛。

那对象的每个成员变量什么时候定义呢?

可是变量整体定义了的话,它的成员不都也定义了吗?
这些成员不都是属于这个对象的吗?

我们运行也没出什么问题。

道理好像是这样的,但是呢?看这种情况:

我们现在给这个类里面再增加一个const的成员变量。
那这时我们再去运行程序:

哦豁,发生错误了,这么回事?
为什么会这样呢?
🆗,大家来想一下,const修饰的变量有什么特点:
const修饰的变量必须在定义的时候赋初值(初始化)
而我们现在有对_b进行初始化吗?
是不是没有啊,我们构造函数都没写,那编译器是会默认生成一个,但是,我们知道默认生成的根本就不会对内置类型进行处理。

那我们是不是自己写个构造函数就行了:


但是我们发现还不行,为什么呢?
因为const变量必须是在定义的时候赋初值,而我们上面说了构造函数里面只是对其赋值,并不是初始化。

那大家可能想到了:

之前文章里我们在讲解构造函数的时候说了,C++11不是允许内置类型成员变量在类中声明的时候可以给缺省值嘛

我们来试一下:

🆗,这样确实不报错了。

但是,这是C++11之前才提出来的,那C++11之前呢?
如何解决这样的问题呢?

现在的问题是什么:

_b必须初始化,即在定义的时候赋初值,但是现在是不是没法搞啊,构造函数里只能对其赋值,并不是初始化。
那我们是不是要给成员变量也找一个定义的位置,不然像const这样的成员变量不好处理。

那成员变量的定义到底是在哪里呢?

我们可以认为,对象定义的时候,其成员变量也就定义了,但是一个对象可能有多个成员,在对象定义的地方也没法给某个成员初始化啊。
怎么办?
🆗,这就是我们接下来要学的东西——初始化列表。

1.1 初始化列表

那面对上面的问题,我们的祖师爷就要去给成员变量找一个定义的地方,那最终找来找去呢,还是把目标锁定在了构造函数。
在构造函数里面呢又搞了一个东西叫做——初始化列表。

初始化列表:

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

举个栗子:

对于上面类中const int _b的初始化我们就可以放在初始化列表进行处理:

class A
{
public:
	A(int a1, int a2, int b)
		:_a1(a1)
		,_a2(a2)
		,_b(b)
	{
	}
private:
	int _a1;
	int _a2;
	const int _b;
};

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


这下我们再运行程序:

就可以了。

当然:

在构造函数体内我们还可以再为成员变量赋值

注意这里成员_b被const修饰,不能再被赋值了。

然后呢,对于初始化列表,还有一些需要我们注意的地方:

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

  2. 以下三种类成员变量,必须放在初始化列表位置进行初始化:
    引用成员变量
    const成员变量
    没有默认构造函数的自定义类型成员

首先const成员变量:

我们上面举的例子就是const成员变量,它必须在定义的时候赋初值,所以必须在初始化列表对其进行初始化(定义的时候赋初值),当然C++11之后可以给缺省值,这样如果没有对它进行初始化编译器就会用缺省值去初始化。

然后还有引用成员变量:

这个我们在之前学习引用的时候就说了:

引用也必须在定义的时候初始化。

最后就是没有默认构造函数的自定义类型成员:

因为默认生成的构造函数对内置类型不做处理,对自定义类型会去调用它对应的默认构造函数(不需要传参的构造函数都是默认构造函数),所以如果自定义类型成员没有默认构造函数我们就需要自己去初始化它。

举个栗子:

class B
{
public:
private:
	int _b;
};
class A
{
private:
	int _a1;
	int _a2;
	B _bb;
};

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

大家看运行这个程序有问题吗?

没有问题,因为对于成员B _bb;来说,会调用它对应的默认构造,类B我们虽然没写构造函数,但是有编译器默认生成的构造函数。
当然如果我们写了不用传参的构造函数,也可以。
但是如果这样:

此时类B是不是没有默认构造函数了。

那这时就不行了。


让_bb在初始化列表调用其构造函数进行初始化,这样就可以了。

  1. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,成员变量都会在初始化列表定义。当然我们说了C++11之后可以给缺省值,这样如果没有对它进行初始化编译器就会用缺省值去初始化。

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

看这个程序:

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

大家思考一下结果是啥?

构造函数参数a接收传过来的1,1先初始化_a1,然后_a1去初始化_a2,所以都是1,是吗?

结果是1和一个随机值。

为什么是这样?

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

所以先初始化_a2,然后是_a1

所以是1和随机值。

1.2 explicit关键字

我们先来看这样一个类:

class A
{
public:
	A(int a)
		:_a1(a)
	{}

private:
	int _a2;
	int _a1;
};

那我们现在想用A这个类去创建对象:

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

这样肯定是可以的,去调它带参的构造函数。

那除此之外呢,其实还可以这样搞:

欸,这种写法是怎么回事?

这个地方为什么A a2 = 1;这样也可以,1是一个整型,怎么可以直接去初始化一个类对象呢?
🆗,那要告诉大家的是这里其实是一个隐式类型转换。
就跟我们之前提到的内置类型之间的隐式类型转换转化是一样的,会产生一个临时变量,我们再来回顾一下:

那这里A a2 = 1是如何转换的呢?
这里呢也会产生一个临时变量,这个临时变量就是用1去构造出来的一个A类型的对象,然后再用这个临时对象去拷贝构造我们的a2。

那我们可以来证明一下,是不是如我所说的那样:

我们来再写一个拷贝构造:

注意拷贝构造也是有初始化列表的,因为拷贝构造函数是构造函数的一个重载形式。
那我们现在运行程序,看A a2 = 1是不是先用1调构造函数创建一个临时变量,然后再调拷贝构造构造a2。
如果是那就跟我们上面说的一样了。

哦豁,构造确实调了,但是后面没去调拷贝构造啊

是我们上面说的不对吗?

🆗,那其实呢,C++编译器针对自定义类型这种产生临时变量的情况,会进行优化
编译器看到你这里先拿1构造一个对象,然后再去调拷贝构造,有点太费事了,干脆优化成一步,直接拿1去构造我们要创建的对象。
当然,不一定所有的编译器都会优化,但是一般比较新一点的编译器在这里都会优化。

但是呢?口说无凭欸!

你凭什么说这里没有优化的话是会产生临时变量的,说不定人家本来就是直接去构造了呢?
那我们再来看这个代码:
A& c = 10;
这样可以吗?

🆗 ,不行直接报错了。
但是:

加个const就行了。

为什么呢?

🆗,这是不是我们之前在常引用那里讲过的啊:
这里产生了临时变量,而临时变量具有常性,所以我们加了const就行了。
欸,那不是说优化了嘛,但是这里是引用就没优化了,直接拿10去构造一个临时对象,然后c就是这个临时对象的引用,所以只有一步构造,就不用优化了。
所以这里确实是会产生临时变量的,上面那种情况确实是进行了优化。
还有一个点就是,一般来说,C++ 中的临时变量在表达式结束之后 (full expression) 就被会销毁,而这里引用去引用一个临时变量的话会延长它的声明周期的。

那上面说了这么一大堆,想告诉大家的是什么呢?

就是 构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,是支持隐式类型转换的(C++98就支持这种语法了)。

这里就可以这样:

那如果我们这里不想让它支持类型转换了,有没有什么办法呢?

🆗,这就要用到我们接下来要学的一个关键字——explicit
我们只需在对应得构造函数前面加上explicit关键字:

然后:

这样写就不行了。

🆗,但是呢,我们刚才说的是对于单参数的构造函数是支持这种类型转换的,那多参数的构造函数呢?


首先我们肯定是可以这样用的:
A a(1, 1);
那这里能不能也像上面那样支持隐式类型转换呢,两个参数的构造函数,那这样去用?

那首先要告诉大家C++98是不支持多参数的构造函数进行隐式类型转换的。
不过呢,C++11对这块进行了扩展,使得多参数的构造函数也可以进行隐式类型转换,但是,要这样写:


用一个大括号括起来。
那同样的道理,如果我们不想让这里支持这种类型转换,对于多参数的构造函数,也是在前面加一个explicit关键字:

用explicit修饰构造函数,将会禁止构造函数的隐式转换。

2. static 成员

我们来看这样一个面试题:

要求我们实现一个类,并在程序中能够计算出一共创建了多少个类对象。

那大家想一下,可以怎么做?

直接去数行不行啊?
直接数是不是有可能不靠谱啊,因为有些创造类对象的地方是不是会进行优化,这个我们上面刚刚讲过。

那还可以怎么搞呢?

大家想一下,要创建一个类对象,有哪些途径,是不是一定是通过构造函数或者拷贝构造搞出来的
那我们是不是可以考虑利用构造函数和拷贝构造来计算创建出来的对象个数啊。

class A
{
public:
	A(int a)
	{
		
	}
	A(const A& aa)
	{
	}
};

假设我们现在创建了这样几个对象:

void func(A a)
{}
int main()
{
	A a1;
	A a2(a1);
	func(a1);
	A a3 = 1;

	return 0;
}

那我们就可以怎么做:

🆗,定义一个全局变量n,初值为0。

然后每次调用构造函数或者拷贝构造创建对象时就让n++,那这样最后n的值是不是就是创建的对象的个数啊。

我们来测试一下:


结果是4,对不对啊。
我们分析一下其实就是4,答案没问题。

但是大家说当前这种方法好吗?

其实是不太好的,为什么?
首先这里我们用了一个全局变量,那首先第一个问题就是它可能会发生命名冲突;其次,全局的话,是不是在哪都能访问它(而C++讲究封装),都可以修改它啊,如果在其它地方不小心++了几次,结果是不是就不准了啊。

那有没有更好一点的方法呢?

那当然是有的。
应该怎么做呢?
🆗,我们把统计个数的这个变量放到类里面,这样它就属于这个类域了,就不会命名冲突了,然后如果不想让它在类外面被访问到,我们可以把它修饰成私有的就行了

但是:

如果直接放到类里面,作为类的一个成员变量:

那它是不是就属于对象了,但我们要统计程序中创建对象的个数,这样我们每次创建一个对象n就会定义一次,是不是不行啊。
不能让它属于每个对象,是不是应该让它属于整个类啊。

2.1 静态成员变量

怎么做呢?

在它前面加一个static修饰,让它成为静态成员变量。
那这样它就不再属于某个具体对象了,而是存储在静态区,为所有类对象所共享


但是我们发现加了static之后报错了,为什么?
因为静态成员变量是不能在这里声明的时候给缺省值的。
非静态成员变量才可以给缺省值。
大家可以想一下嘛,缺省值其实是在什么时候用的,在初始化列表用的,用来初始化对象的成员变量的,而静态成员变量我们说了,它是属于整个类的,被所有对象所共享。

类里面的是声明,那静态成员变量的初始化应该在哪?

🆗,规定静态成员变量的初始化(定义的时候赋初值)一定要在类外,定义时不添加static关键字,类中只是声明。

但是现在又有一个问题:


我们把它搞成私有的,在外面就不能访问了。
当然如果不加private修饰就可以了:

另外呢,这里除了指定类域来访问静态成员变量,还可以通过对象去访问:

因为它属于整个类,并且被所有对象共享。
还可以这样:

这个问题我们之前是不是说过啊,不能看到->或者.就认为一定存在解引用,还是要根据具体情况进行分析。
当然如果是私有的情况下,这样写是不是统统不行啊:

那我们就可以写一个Get方法:


成员函数是需要通过对象去调用的
这样就可以了。

那如果我们的程序是这样的呢?


在main函数里面我们根本没有创建对象,那我们还怎么调用Getn函数呢?
难道我们专门在main函数里创建一个对象去调用Getn,然后再把结果减1:

因为main函数里的对象是我们为了调用函数而创建的对象,所以最后要减去。

2.1 静态成员函数

那有没有什么办法可以不通过对象就能调用到Getn函数呢?

那我们就可以把Getn函数搞成静态成员函数,也是在前面加一个static关键字就行了。

但是静态成员函数有一个特性:静态成员函数没有隐藏的this指针,不能访问任何非静态成员。
因为非静态成员是属于对象的,都是通过this指针去访问的,而静态成员函数是没有this指针的。


那它没有this指针,就可以不通过对象调用了,所以现在我们通过指定类域也可以调用到静态的Getn函数。

当然你还通过对象调用也还是可以的。

那我们现在在加一个东西:


大家觉得现在结果会多几个对象:

🆗,13个,是不是比之前多了10个啊,因为我们又多定义了一个大小为10 的类对象数组。

2.3 练习

那接下来我们来做个题: link

这道题呢就是让我们求一个1到n的和,但是要求了一大堆,不让用这,不让用那的。
🆗,那不用就不用呗,其实借助我们刚才学的知识,就可以很巧妙的去解这道题。

怎么做呢?


我们自己呢定义这样一个类,两个静态成员变量_i_sum,分别初始化为0,1。
然后我们调用一次构造函数,就让_sum+=_i,然后_i++,这样第一次+1,第二次+2…
那现在我们要求1+2+3…+n的和,怎么办?
是不是是需要调用n次构造函数,所以,我们直接定义一个大小为n的类对象数组就行了。

class Sum{
public:
    Sum()
    {
        _sum +=_i;
        ++_i;
    }
    static int GetSum()
    {
        return _sum;
    }
private:
    static int _i;
    static int _sum;
};
int Sum::_i=1;
int Sum::_sum=0;

class Solution {
public:
    int Sum_Solution(int n) {
        Sum arr[n];//C99支持变长数组,可以用变量指定数组大小,但不能初始化。
        return Sum::GetSum();
    }
};

这样就行了。

2.4 总结

那最后我们来总结一下:

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。

特性:

  1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
  2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明,静态成员变量一定要在类外进行初始化
  3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制

3. 匿名对象

接下来我们再来学一个知识叫做匿名对象,什么是匿名对象呢?

那现在呢有这样一个类:

我们现在想要那这个类去创建对象,那除了我们之前学的方法之外,其实我们还可以这样创建对象:

🆗,这里我们就拿A这个类创建了一个匿名对象
匿名对象的特点就是没有名字,但是它的生命周期只在创建它的这一行。
我们可以来证明一下:

我们通过调式可以发现,113行执行完,这个匿名对象就已经调用了它的析构函数,即它的声明周期已经结束了。
但是要注意,和临时变量一样,如果我们用匿名对象去初始化一个引用的话,它的生命周期就会被延长至该引用被销毁。并且这里肯定都要加const的,因为临时变量和匿名对象都具有常性。

那匿名对象有什么用呢?

既然创造出来,就一定是有用的。
比如:

现在有一个类Solution,里面有一个非静态成员函数Sum_Solution,我们知道想要调用类里面的非静态成员函数,是需要通过对象去调用的。

那现在有了匿名对象,我们就可以这样调用了:

匿名对象在这样场景下就很好用,当然还有一些其他使用场景,这个我们以后遇到了再说。

4. 友元

友元分为:友元函数和友元类。

4.1 友元函数

那友元函数我们在上一篇文章是不是就用到了:

在上一篇文章我们实现的日期类中:
我们尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。

但是实际使用中cout需要是第一个形参对象,才能正常使用。

所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,然后我们就使用友元解决了。operator>>同理。


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

说明:

  1. 友元函数可访问类的私有和保护成员,但不是类的成员函数
  2. 友元函数不能用const修饰
    经过之前的学习我们知道const修饰的是啥?
    const修饰非静态成员函数,实际修饰的是this指针,而友元函数根本都不是类成员函数,所以都没有this指针。
  3. 友元函数可以在类里面的任何地方声明,不受类访问限定符限制
  4. 一个函数可以是多个类的友元函数
  5. 友元函数的调用与普通函数的调用原理相同

4.2 友元类

接下来我们再来学习一下友元类:

我们来看这样一个场景:

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

现在有两个类,在Date类中有一个成员变量是Time类的对象。
Time类中的成员变量都是私有的,那在Date类中我们想访问Time类成员的私有成员变量,是不行的。

那想解决这个问题,除了去写Get和Set方法,还可以这样解决:
就是声明日期类为时间类的友元类,这样在日期类中就可以直接访问Time类成员中的私有成员变量了。

然后呢,友元类还有一些需要我们注意的地方:

  1. 友元关系是单向的,不具有交换性

比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。

  1. 友元关系不能传递

若C是B的友元, B是A的友元,但不能说明C是A的友元。

  1. 友元关系不能继承,这个在后面讲到继承的时候再给大家详细介绍

5. 内部类

我们再来看一个东西叫做内部类,什么是内部类呢?

如果一个类定义在另一个类的内部,那么这个类就叫做内部类。

比如像这样:

class A
{
private:
	int h;
public:
	class B 
	{
	private:
		int b;
	};
};

那大家先来思考一个问题:类A的大小是多少?

对于类A来说,首先有一个整型成员变量h,然后还有一个成员是类B,B里面也有一个整型成员变量。
那两个整型变量,A的大小是不是8个字节啊?
我们来验证一下:

欸,结果是4,为什么呢,里面不是还有一个类B嘛。
我们通过调式可以发现:

拿A定义一个对象,它的里面只有一个成员变量h,并没有类B。

所以呢:

这里想告诉大家的是:
内部类并不属于外部类,它和对应的外部类是相互独立的,只是受外部类类域的限制。
对于上面那个类来说,我们想拿A中的内部类B去创建对象,这样是不行的:

因为B是在A这个类域里面的。

这样就可以了。

另外呢:

内部类也是受访问限定符的限制的。
刚才类B在A中是Public修饰的,所以我们指定类域之后就可以访问了,但如果B被private修饰呢?

就不行了。

然后还有就是:

内部类天生就是其对应的外部类的友元类。

那这样的话,在B中就可以直接访问A中的私有成员了。

注意内部类可以直接访问外部类中的static成员,不需要通过外部类的对象/类名。
但是我们说了,友元关系是单向的,所以:
外部类对内部类没有任何的访问权限。

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

在有些拷贝对象的情况下,C++编译器会做一些优化,减少对象的拷贝,这个在有些场景下还是非常有用的。

那其实在上面我们已经提到过一种场景了:


我们说这种场景会发生一个隐式类型转换,先拿1去构造一个临时对象,然后再拷贝构造给对象a。
但是呢,编译器会进行一个优化,直接拿1去构造对象a。

那除此之外,在某些传参和传返回值的过程,也会有这样的优化。

来看这个类:

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

看这个场景:


大家思考一下,在调用fun1传参的过程中,这里会发生优化吗?
我们来分析一下,这里正常的逻辑是先拿2去构造一个临时对象,然后再去拷贝构造形参a。
那这里肯定也是会直接优化成一步构造的。

我们可以来验证一下:

是不是只有一步构造啊,这里析构的其实是fun1中的a。

还有这种情况也是同样的道理。
当然这几种情况如果我们传的是引用的话那就不用拷贝了,所以传参能用引用的话可以尽量传引用。

再来看这种场景:


main函数里面只调用了一下fun2,在fun2函数里面,先是一个构造,然后是一个拷贝构造(因为a出函数就销毁了,返回的是一个临时变量,是a的拷贝,这个也是我们前面讲过的知识)。
那这里会优化吗?
不会的,因为这里的构造和拷贝构造并不是一个表达式里的,是分开的两步。当然不同的编译器也可能会不同处理。
我们可以调式观察一下:

那我们如果写成这样呢:


这里跟上面那样写相比是不是又多了一个拷贝构造啊。
返回值返回是一个拷贝构造,然后紧接着又把返回值拷贝构造给了aa。
那这里会优化吗?
会的,因为这两个拷贝构造是不是一个连续的过程啊。

可以看到,这里跟上面是一样的。当然这是编译器优化的结果。

🆗,那如果我们接收返回值这样接收呢:


我们先定义一个对象,然后拿定义好的对象去接收返回值。
这两种写法有什么不同呢,我们来对比一下:

大家可以看一下,差别还是挺大的。
第一种写法呢是构造(函数内)+一个拷贝构造(返回值),优化之后是这样;但是第二种的话是首先main函数内构造一个对象aa2,然后函数内一个构造,一个拷贝构造,最后又赋值给aa2。
所以说:
接收对象返回值的时候,尽量用拷贝构造的方式接收,不要赋值接收。

再来看,这样呢:


刚才我们是先构造一个对象,然后返回,那如果现在直接返回一个匿名对象呢?
那这样的话,匿名对象的构造和返回时的拷贝构造是不是就连续了,所以这里编译器就会对它进行优化了:

直接返回匿名对象的情况下,构造+拷贝构造就被优化成直接构造了。
所以在返回对象时,能用匿名对象的话可以选择用匿名对象。

7. 再次理解类和对象

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

比如想要让计算机认识洗衣机,就需要:

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

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

🆗,那我们这篇文章的内容就到这里,欢迎大家指正!!!

有关【C++】类和对象(完结篇)的更多相关文章

  1. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  2. ruby-on-rails - 按天对 Mongoid 对象进行分组 - 2

    在控制台中反复尝试之后,我想到了这种方法,可以按发生日期对类似activerecord的(Mongoid)对象进行分组。我不确定这是完成此任务的最佳方法,但它确实有效。有没有人有更好的建议,或者这是一个很好的方法?#eventsisanarrayofactiverecord-likeobjectsthatincludeatimeattributeevents.map{|event|#converteventsarrayintoanarrayofhasheswiththedayofthemonthandtheevent{:number=>event.time.day,:event=>ev

  3. ruby-on-rails - 如何验证非模型(甚至非对象)字段 - 2

    我有一个表单,其中有很多字段取自数组(而不是模型或对象)。我如何验证这些字段的存在?solve_problem_pathdo|f|%>... 最佳答案 创建一个简单的类来包装请求参数并使用ActiveModel::Validations。#definedsomewhere,atthesimplest:require'ostruct'classSolvetrue#youcouldevencheckthesolutionwithavalidatorvalidatedoerrors.add(:base,"WRONG!!!")unlesss

  4. Ruby 写入和读取对象到文件 - 2

    好的,所以我的目标是轻松地将一些数据保存到磁盘以备后用。您如何简单地写入然后读取一个对象?所以如果我有一个简单的类classCattr_accessor:a,:bdefinitialize(a,b)@a,@b=a,bendend所以如果我从中非常快地制作一个objobj=C.new("foo","bar")#justgaveitsomerandomvalues然后我可以把它变成一个kindaidstring=obj.to_s#whichreturns""我终于可以将此字符串打印到文件或其他内容中。我的问题是,我该如何再次将这个id变回一个对象?我知道我可以自己挑选信息并制作一个接受该信

  5. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

  6. ruby-on-rails - 未在 Ruby 中初始化的对象 - 2

    我在Rails工作并有以下类(class):classPlayer当我运行时bundleexecrailsconsole然后尝试:a=Player.new("me",5.0,"UCLA")我回来了:=>#我不知道为什么Player对象不会在这里初始化。关于可能导致此问题的操作/解释的任何建议?谢谢,马里奥格 最佳答案 havenoideawhythePlayerobjectwouldn'tbeinitializedhere它没有初始化很简单,因为你还没有初始化它!您已经覆盖了ActiveRecord::Base初始化方法,但您没有调

  7. ruby - 如何在 Rails 4 中使用表单对象之前的验证回调? - 2

    我有一个服务模型/表及其注册表。在表单中,我几乎拥有服务的所有字段,但我想在验证服务对象之前自动设置其中一些值。示例:--服务Controller#创建Action:defcreate@service=Service.new@service_form=ServiceFormObject.new(@service)@service_form.validate(params[:service_form_object])and@service_form.saverespond_with(@service_form,location:admin_services_path)end在验证@ser

  8. Ruby——嵌套类和子类是一回事吗? - 2

    下面例子中的Nested和Child有什么区别?是否只是同一事物的不同语法?classParentclassNested...endendclassChild 最佳答案 不,它们是不同的。嵌套:Computer之外的“Processor”类只能作为Computer::Processor访问。嵌套为内部类(namespace)提供上下文。对于ruby​​解释器Computer和Computer::Processor只是两个独立的类。classComputerclassProcessor#Tocreateanobjectforthisc

  9. ruby - 一个 YAML 对象可以引用另一个吗? - 2

    我想让一个yaml对象引用另一个,如下所示:intro:"Hello,dearuser."registration:$introThanksforregistering!new_message:$introYouhaveanewmessage!上面的语法只是它如何工作的一个例子(这也是它在thiscpanmodule中的工作方式。)我正在使用标准的ruby​​yaml解析器。这可能吗? 最佳答案 一些yaml对象确实引用了其他对象:irb>require'yaml'#=>trueirb>str="hello"#=>"hello"ir

  10. ruby - 更改 ActiveRecord 中对象的类 - 2

    假设我有一个FireNinja我的数据库中的对象,使用单表继承存储。后来才知道他真的是WaterNinja.将他更改为不同的子类的最干净的方法是什么?更好的是,我很想创建一个新的WaterNinja对象并替换旧的FireNinja在数据库中,保留ID。编辑我知道如何创建新的WaterNinja来self现有FireNinja的对象,我也知道我可以删除旧的并保存新的。我想做的是改变现有项目的类别。我是通过创建一个新对象并执行一些ActiveRecord魔法来替换行,还是通过对对象本身做一些疯狂的事情,或者甚至通过删除它并使用相同的ID重新插入来做到这一点,这是问题的一部分。

随机推荐