C++小白的逆袭之路——初阶(第四章:类和对象下)


4.1再谈构造函数


4.1.1构造函数体赋值


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

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

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


4.1.2初始化列表


1.引入:

看下面一段代码:

class A
{
public:

	A(int a, int& b)
	{
		_a = a;		// 报错
		_b = b;		// 报错
	}

	void func()
	{
		++_b;
		++_b;
	}

private:
	// 成员函数
	const int _a;
	int& _b;	
};

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

为什么会报错?

const类型和引用类型的变量只能在定义的时候初始化,我们已经知道了,在构造函数内所写的_a = a; _b = b;是赋值而不是初始化,所以自然就编不过了。那怎么办?引入一个新概念:初始化列表。

2.初始化列表的写法:

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

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
{
public:

	A(int a, int& b)
		:_a(a)
		,_b(b)
	{}

	void func()
	{
		++_b;
		++_b;
	}

private:
	// 成员函数
	// 必须在定义的地方初始化
	const int _a;
	int& _b;	// 引用也必须在定义的地方初始化
};

int main()
{
	int n = 0;
	A a1(1, n);

	a1.func();
	cout << n << endl; // n变成了2
	return 0;
}

3.注意(总结):

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

2)类中包含以下成员,必须在初始化列表位置初始化:

  • const成员变量
  • 引用成员变量
  • 自定义类型成员,且该类没有默认构造函数时(但必须有构造函数)
class A
{
public:
	A(int a)			// 有构造函数,但不是默认的
		:_a(a)
	{}
private:
	int _a;
};

class B
{
public:
	B(int a, int ref)
		:_aobj(a)		// 不写这句代码直接报错
		,_ref(ref)
		,_n(10)
	{}
private:
	A _aobj;		 	// 没有默认构造函数
	int& _ref; 			// 引用
	const int _n; 		// const
};

int mian()
{
	B b(1, 2);
	return 0;
}

如果没有初始化_aobj,没有:_aobj(a)这句代码,程序直接报错。因为_aobj是自定义类型的成员,且它没有默认构造函数,只有构造函数,只能使用这个构造函数来进行初始化。

3)初始化列表是每个成员定义的地方,不管你写不写,每个成员都要走初始化列表,所以每个成员最好都用初始化列表初始化。

class Time
{
public:
	Time(int hour = 0)
		:_hour(hour)
	{}
private:
		int _hour;
};

class Date
{
public:
	Date(int day)
	{}
private:
	int _day;
	Time _t;
};

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

执行构造函数时,先执行初始化列表中的内容,如果有的成员没有写进初始化列表,就会被编译器自行处理:内置类型成员不做处理,自定义类型的变量用默认的构造函数初始化。

执行Date d1(1);时,相当于执行Date类的构造函数。发现Date的构造函数中没有定义初始化列表,根据默认情况,内置类型成员_day不做处理,自定义类型成员_t会去调用自己的默认构造函数初始化。其中Time类型的构造函数有自定义的初始化列表,所以先执行这个初始化列表,让hour的值给_hour。

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

// 下面代码运行的结果是什么?
class C
{
public:
	C(int a)
		: _a1(a)
		, _a2(_a1)
	{}
	void Print() 
	{
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};

int main()
{
	C aa(1);
	aa.Print();
	return 0;
}

上面代码运行的一个结果:1 -858993460其中_a2是随机值。为什么?

执行C的构造函数时,先走初始化列表。而初始化列表中,成员变量初始化的执行顺序和成员变量在初始化列表中的位置无关,只和成员变量声明的顺序有关。
对于上面的这段代码:执行C的初始化列表时,先执行的代码是:_a2(a1),因为_a2最先声明。而_a1此时是随机值,把_a1的值赋给_a2,_a2就是随机值。后执行的代码是:_a1(a),我们传给a的值是1,所以把a的值赋给_a1,_a1自然也是1。上面的输出结果就是这么来的。

4.初始化列表一般会和构造函数体混合使用:

写一个栈的构造函数:

class Stack
{
	//成员函数
public:
	//构造函数
	Stack(int capacity = 4)				//给缺省值,如果什么也不传,就默认开辟4个数据的空间
		: _a((STDataType*)malloc(sizeof(STDataType) * capacity))
		, _top(0)
		, _capacity(capacity)
	{
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}
		memset(_a, 0, sizeof(DateType) * _capacity);
	}
	
	//成员变量
private:
	STDataType* _a;
	int _top;
	int _capacity;
};

4.1.3explicit关键字


1.单参数构造函数的隐式类型转换:

class A
{
public:
	A(int i)
		:_a(i)
	{}

	A(const A& aa)
		:_a(aa._a)
	{}

private:
	int _a;
};

int main()
{
	A aa1(1);
	A aa2 = 2;		// 这句代码是单参数的隐式类型转换
	
	A& ref = 2;		// 报错,权限放大
	const A& reftt = 2;		// 可以
	return 0;
}

编译器在执行A aa2 = 2;这句代码时,会先用2调用A的构造函数,去生成一个临时对象,再用这个对象去拷贝构造aa2。有的编译器会优化,直接用2调用构造函数去构造aa2。验证:

上述代码,执行A& ref = 2;时,会报错。因为编译器先用2调用构造函数去生成了一个临时对象,而临时对象是具有常性的,传给没有const修饰的引用ret属于权限的放大,所以编不过;而传给用const修饰的rett就不会报错,因为这属于权限的平移。

2.浅看一下隐式类型转化有什么用:

class A
{
public:
	A(int i)
		:_a(i)
	{}

	A(const A& aa)
		:_a(aa._a)
	{}

private:
	int _a;
};

typedef A DataType;

// 顺序表中存的是A类型的数据
class SeqList
{
public:
	// 成员函数
	SeqList(size_t capacity = 10)
		: _array((DataType*)malloc(sizeof(DataType)* capacity))
		, _capacity(capacity)
		, _size(0)
	{
		memset(_array, 0, sizeof(DataType) * _capacity);
	}

	void Push(const DataType& x)	{	_array[_size++] = x; }


private:
	// 成员变量
	DataType* _array;
	size_t _capacity;
	size_t _size;
};

int main()
{
	SeqList s;

	A aa1(1);		// 没有隐式类型转换时需要先创建一个A类型的变量,再将这个变量传入s
	s.Push(aa1);

	s.Push(2);		// 有了隐式类型转换就可以这样写,不用再创建一个A类型的变量了
	return 0;
}

C++11还支持多参数隐式类型转换:

class A
{	
public:
	A(int a, int b)
		:_a(a)
		,_b(b)
	{}

	A(const A& aa)
		:_a(aa._a)
		,_b(aa._b)
	{}
	
private:
	int _a;
	int _b;
};

typedef A DataType;

class SeqList
{
public:
	// 成员函数
	SeqList(size_t capacity = 10)
		: _array((DataType*)malloc(sizeof(DataType)* capacity))
		, _capacity(capacity)
		, _size(0)
	{
		memset(_array, 0, sizeof(DataType) * _capacity);
	}

	void Push(const DataType& x)	{	_array[_size++] = x; }


private:
	// 成员变量
	DataType* _array;
	size_t _capacity;
	size_t _size;
};

int main()
{
	SeqList s;	

	A aa1 = { 2, 3 };	// 使用一个大括号,来实现多参数的隐式类型转换

	s.Push({1, 2});		
	return 0;
}

使用一个{},来实现隐式类型的转换。

3.explicit的作用:

禁止隐式类型转换的发生:

class A
{
public:
	explicit A(int i)		// 在构造函数前加了explicit,就不能再使用隐式类型转换了
		:_a(i)
	{}

	A(const A& aa)
		:_a(aa._a)
	{}

private:
	int _a;
};

int main()
{
	A aa1(1);

	// 下面两句代码全都编不过
	A aa2 = 2;		
	const A& reftt = 2;		
	return 0;
}

4.2static成员


1.引入:

如何知道创建了多少个某类类型的变量?以及正在使用中的某类类型的变量?

1)方法1:

// 正在使用的
int m = 0;
// 累计创建了多少个对象
int n = 0;

class A
{
public:
	A()
	{
		++m;
		++n;
	}

	A(const A& t)
	{
		++m;
		++n;
	}

	~A()
	{
		--m;
	}
};

const A& Func(const A& aa)
{
	return aa;
}

int main()
{
	A aa1;
	A aa2;

	cout << "n:" << n << " m:" << m << endl;

	A();
	cout << "n:" << n << " m:" << m << endl;

	Func(aa1);
	cout << "n:" << n << " m:" << m << endl;

	return 0;
}

输出结果:

n:2 m:2
n:3 m:2
n:3 m:2

使用全局变量m来记录正在使用的A类型变量个数,n来记录累计创建的A类型变量个数。创建新变量时,肯定要用到构造函数或拷贝构造函数,那么我们可以在这两个函数中,让mn++,此时mn都记录的是累计创建的A类型变量个数。如何记录正在使用的A类型变量个数呢?因为自定义类型变量出了作用域就会销毁,所以在析构函数中对m--就可以了。

思考:上面这种写法有什么缺陷?

容易被动手脚

// 类还是上面的类,只写了主函数
int main()
{
	A aa1;
	A aa2;

	cout << "n:" << n << " m:" << m << endl;

	// 动手脚
	--n;
	--m;

	A();
	cout << "n:" << n << " m:" << m << endl;

	Func(aa1);
	cout << "n:" << n << " m:" << m << endl;

	return 0;
}

输出结果改变:

n:2 m:2
n:2 m:1
n:2 m:1

2)方法2:(n和m是公有的)

class A
{
public:
	A()
	{
		++m;
		++n;
	}

	A(const A& t)
	{
		++m;
		++n;
	}

	~A()
	{
		--m;
	}

// m和n用public修饰
	// 声明
	// 正在使用的
	static int m;
	// 累计创建了多少个对象
	static int n;
};

// 定义
int A::n = 0;
int A::m = 0;
// 注意,在类中定义的static修饰的变量,不能在声明处给缺省值
// 因为声明处给的缺省值是在初始化列表中使用的,而静态成员变量不会走初始化列表,所以不能在声明处给缺省值

int main()
{
	A aa1;
	A aa2;

	// 直接访问n和m不可以
	// cout << "n:" << n << " m:" << m << endl;

	// 可以这样访问
	cout << "n:" << A::n << " m:" << A::m << endl;
	
	// 也可以这样访问(因为静态成员变量属于所有A对象,属于整个类)
	cout << "n:" << aa1.n << " m:" << aa2.m << endl;

	// 不能像之前一样动手脚了
	/*
	--m;
	--n;
	*/
	// 报错
	
	// 可以这样动手脚
	--A::n;
	--aa2.m;

	cout << "n:" << aa1.n << " m:" << aa2.m << endl;

	return 0;
}

输出结果:

n:2 m:2
n:2 m:2
n:1 m:1

注意:

1)静态成员变量属于所有A对象,属于整个类。
2)虽然不能像之前一样那么明目张胆的改变n和m,但是也是有方法可以改变的。
3)通过aa1或aa2改变n和m,结果是一样的,改变的是同一个n,同一个m,因为静态成员变量属于所有A对象,是大家共同的成员变量。
4)在类中定义的static修饰的变量,不能在声明处给缺省值:不能在声明处直接写static int n = 0;。因为声明处给的缺省值是在初始化列表中使用的,而静态成员变量不会走初始化列表,所以不能在声明处给缺省值。

思考:创建一个A*类型的指针变量,赋值为空,可以像下面这样访问n和m吗?

int main()
{
	A aa1;
	A aa2;
	
	cout << "n:" << aa1.n << " m:" << aa2.m << endl;

	// 像下面这样去访问n和m
	A* ptr = nullptr;
	ptr->m++;
	ptr->n++;
	cout << "n:" << aa1.n << " m:" << aa2.m << endl;
	
	return 0;
}

输出结果:

n:2 m:2
n:3 m:3

结论:访问成功,不报错(和用空指针去访问成员函数是同样的道理,这是为什么?)

在这段代码中,看似发生了控制指针的解引用,实则没有,编译器没有那样做。因为static修饰的变量是存储在静态区的,你写下ptr->n这样的代码,其实从底层来看,和A::n是同样的作用,都是用来突破类域的写法,告诉编译器要去哪个类中去找这个成员。

当然,如何n和m用private修饰,上面所有的访问方式就都行不通了。但是,用private修饰才是正确的方法,这样就可以彻底防止被动小动作(人为改变n和m),我们要查看n和m的值,只需要写一个公有的函数就可以了。

class A
{
public:
	A()
	{
		++_m;
		++_n;
	}

	A(const A& t)
	{
		++_m;
		++_n;
	}

	~A()
	{
		--_m;
	}

	void Print()
	{
		cout << "n:" << _n << " m:" << _m << endl;
	}

private:
	static int _m;
	static int _n;
};

// 静态成员变量一定要在类外初始化
int A::_m = 0;
int A::_n = 0;

int main()
{
	A aa1;
	A aa2;

	// A::Print();  报错
	aa1.Print();

	return 0;
}

思考:能不能匿名成员来使用Print()函数?
  代码:A().Print();

不行,因为为了使用这个打印功能,就多创建了一个对象,干扰了我们正常的逻辑。

要想用A::Print()这样的方式使用打印函数怎么办?

使用静态成员函数:

static void Print()
{
	cout << "n:" << _n << " m:" << _m << endl;
}

这样就可以使用A::Print()了。

静态成员函数的特点:没有this指针

class A
{
public:
	A()
	{
		++_m;
		++_n;
	}

	A(const A& t)
	{
		++_m;
		++_n;
	}

	~A()
	{
		--_m;
	}

	// 静态成员函数的特点是没有this指针
	static void Print()
	{
		_x++;		// 报错,因为没有this指针
		cout << "n:" << _n << " m:" << _m << endl; // 可以访问非静态成员变量
	}

private:
	static int _m;
	static int _n;
	int _x = 0;
};

int A::_m = 0;
int A::_n = 0;

int main()
{
	A aa1;
	A aa2;

	A::Print(); 
	aa1.Print();

	return 0;
}

非静态成员变量,需要使用this指针来访问,而静态成员函数没有this指针,自然无法访问非静态成员变量。

2.概念:

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

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

4.看一道例题:
题目:

求1+2+3+…+n,要求不能使用乘除法,不能使用forwhileifelseswitchcase等关键字及条件判断语句(A?B:C)。

分析问题:

限制了任何形式的循环,也因为不能使用forwhile;限制了递归,因为递归中很重要的一步就是确定递归的边界,而这种边界条件的设置,需要借助if等条件判断语句来完成。

代码:

class Sum
{
public:
	Sum()
	{
		_ret += _i;
		_i++;
	}

	static int Getret()
	{
		return _ret;
	}

private:
	static int _i;
	static int _ret;
};

int Sum::_i = 1;
int Sum::_ret = 0;

class Soution
{
public:
	int Sum_Solution(int n)
	{
		Sum a[n];		 // 变长数组
		return Sum::Getret();
	}
};

int main()
{
	cout<< Soultion().Sum_Soution(10) << endl;
	return 0;
}

分析代码:

我们可以利用静态成员,通过重复调用构造函数的方式,来完成该题目。
1)首先,先创建一个自定义类型Sum,里面包含两个静态成员变量_i_ret_i记录从1到n的变化,每走一次构造函数,就++一次;_ret记录1+2+…+n的结果。_i的初始值为1,_ret的初始值为0。
2)其次,我们怎么实现构造函数的多次调用呢?现在绝大多数编译器都支持变长数组,我们可以通过创建一个Sum类型数组的方式,实现构造函数的多次调用。Sum a[n];
3)最后,我们要返回_ret的值,而_ret是私有的,我们需要写一个成员函数来,通过调用这个成员函数来访问它。之所以选择静态成员函数,是因为这样就可以用类名加::加函数名的方式(Sum::Getret())来使用这个函数了。


4.3友元


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


4.3.1友元函数


1.问题:

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

2.解决:

使用友元关键字friend,在类外定义函数:

class Time
{
	// 友元函数
	friend ostream& operator<<(ostream& out, const Time& t);
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		: _hour(hour)
		, _minute(minute)
		, _second(second)
	{}

private:
	int _hour;
	int _minute;
	int _second;
};

ostream& operator<<(ostream& out, const Time& t)
{
	out << t._hour << "/" << t._minute << "/" << t._second << endl;
	return out;
}

int main()
{
	Time t(12, 30, 6);
	cout << t << endl;
	return 0;
}

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

3.说明:

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


4.3.2友元类


1.介绍:

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

class Time
{
	// 声明Date类为Time类的友元,这样就可以在Date中直接访问Time的私有成员了
	friend class Date;
	
	// 友元函数
	friend ostream& operator<<(ostream& out, const Time& t);
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
{
	friend ostream& operator<<(ostream& out, const Date& d);

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

	// 直接访问了Time类的私有成员
	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;
};

ostream& operator<<(ostream& out, const Time& t)
{
	out << t._hour << "/" << t._minute << "/" << t._second << endl;
	return out;
}

ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "/" << d._month << "/" << d._day << endl;
	out << d._t << endl;
	return out;
}

int main()
{
	Date d1(2023, 9, 23);
	cout << d1 << endl;
	d1.SetTimeofDate(12, 2, 34);
	cout << d1 << endl;
	return 0;
}

2.说明:

1)友元关系是单向的,不具有交换性。(我把你当朋友,你不一定把我当朋友)

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

2)友元关系不能传递。(你的朋友不是我的朋友)

如果C是B的友元, B是A的友元,则不能说明C时A的友元。

3)友元关系不能继承,在继承位置再给大家详细介绍。

3.总结:

不到万不得已,不要使用友元。


4.4内部类


1.概念:

在一个类内部定义的类,就叫内部类:

class A
{
public:
	// B是内部类
	class B
	{
	private:
		int _b;
	};
	// B类受A类域和访问限定符的限制,其实他们是两个独立的类
private:
	int _a;
};

如何理解“B类受A类域和访问限定符的限制,其实他们是两个独立的类”?

1)如果要创建一个B类型的变量,需要这样写:A::B b;,而且B类必须在A类中用public修饰。如果用privateprotected修饰,那么就不能像上面那么写了,因为没有对B类的访问权限。

2)用siezof查看A类的大小:

class A
{
public:
	class B
	{
	private:
		int _b;
	};

private:
	int _a;
};

int main()
{
	cout << sizeof(A) << endl;	// 输出结果是4,只有_a的大小
	return 0;
}

发现输出结果是4,说明B在存储这一概念上并没有存在A类中,A类和B类是两个独立的类。

2.内部类就是外部类的友元

class A
{
public:
	class B
	{
	public:
		void func(A a, int b)
		{
			a._a = 10;	// 可以直接访问A的私有成员
			_b = b;
		}
	private:
		int _b;
	};

private:
	int _a;
};

内部类可以访问外部类,但外部类不可以访问内部类。

3.用内部类改进之前static的练习题

class Solution
{
	class Sum
	{
	public:
		Sum()
		{
			_ret += _i;
			_i++;
		}
	};

public:
	int Sum_solution(int n)
	{
		Sum a[n];
		return _ret;
	}

private:
	static int _i;
	static int _ret;
};

int Solution::_i = 1;
int Solution::_ret = 0;

内部类的使用使代码更加简洁了。


4.5匿名对象


1.匿名对象的定义:

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

int main()
{
	// 有名对象
	// 特点:生命周期在当前局部域
	A a1;

	// 匿名对象
	// 特点:生命周期只在这一行
	A(7);
	
	return 0;
}

不要认为匿名对象没有用,事实上,匿名对象的使用非常广泛。

2.匿名对象的使用场景:

class Solution
{
public:
	int sum(int x, int y)
	{
		return x + y;
	}
};

int main()
{

	// 正常情况下调用这个Solution
	Solution sl;
	sl.sum(10, 20);

	// 利用匿名对象去调用sum
	Solution().sum(10, 29);
	return 0;
}

上面的代码中,Solution()就是定义了一个匿名对象。有了匿名对象,我们就不用为了使用Soultion类里的功能,再多写一行代码去创建一个有名函数sl了。

3.临时对象和匿名对象都具有常属性,无法更改

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

void f1(A& aa)
{
	
}

int main()
{
	f1(A());	// 报错
	return 0;
}

当临时对象作为引用传给f1后,是权限的放大,导致这个临时对象可以被修改了,直接报错。要想正常传参,要这样写void f1(const A& aa)

4.匿名对象的生命周期只在当前这一行,而const加引用的方式会延长匿名对象的生命周期

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

int mian()
{
	// const引用会延长匿名对象生命周期
	// ret出了作用域,匿名对象才销毁
	const A& ret = A();

	return 0;
}
	

const A& ret = A();本质上是给匿名对象又取了一个名字,叫ret


4.6拷贝对象时的一些编译器优化(了解学习)


在传参和传返回值的过程中,编译器一般会做一些优化(大部分编译器都会优化),减少拷贝构造函数的调用,来看一些例子:

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

以下的例子都是根据以上代码运行的。

1.来复习一下拷贝构造函数调用的场景:

int main()
{
	// 传值传参
	A aa1;
	f1(aa1);
	cout << "————————————————————" << endl;

	// 传值返回
	f2();
	cout << "————————————————————" << endl;
}

输出结果:

A(int a)
A(const A& aa)
~A()
————————————————————
A(int a)
~A()
————————————————————
~A()

分析:

1)代码A aa1;先调用了一次构造函数A(int a),初始化变量aa1。接着将aa1传给形参aa时,是一个传值调用,调用拷贝构造函数A(const A& aa)。出函数f1时,调用析构函数~A(),析构形参aa。然后打印一条横线。
2)执行函数f2时,在函数内,代码A aa;先调用了一次构造函数A(int a)returnaa时,实际上会把aa的值用拷贝构造函数先储存在一个临时变量中,这里没有显示的打印出来。然后aa出作用域,调用析构函数~A()销毁。
3)最后,主函数结束,aa1出作用域,调用析构~A()销毁。

2.几种编译器优化的情况分析:

1)情况一:

int main()
{
	// 隐式类型,连续构造+拷贝构造->优化为直接构造
	f1(1);
	// 一个表达式中,连续构造+拷贝构造->优化为一个构造
	f1(A(2));
	cout << endl;
	return 0;
}

输出结果:

A(int a)
~A()
A(int a)
~A()

分析:

1)没有优化的情况下,执行代码f1(1);,会先调用一次构造函数A(int a),进行隐式类型转化,生成一个临时变量。再调用一次拷贝构造A(const A& aa),将临时变量拷贝给aa。但是打印结果却并非如此,经过优化,编译器只调用了一次构造函数A(int a),将连续构造+拷贝构造优化成了直接构造。
2)没有优化的情况下,执行代码f1(A(2));,会先调用一次构造函数A(int a),给匿名对象初始化。然后再调用一次拷贝构造A(const A& aa),将匿名对象拷贝给aa。但是打印结果并非如此,经过优化,编译器只调用了一次构造函数A(int a),将连续构造+拷贝构造优化为了一个构造。

2)情况二:

int main()
{
	// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
	A aa2 = f2();
	cout << endl;
	// 多个表达式中,连续拷贝构造+赋值重载->无法优化
	A aa1;
	aa1 = f2();
	cout << endl;
	return 0;
}

输出:

A(int a)

A(int a)
A(int a)
A& operator=(const A& aa)
~A()

~A()
~A()

分析:

1)在没有优化的情况下,执行代码A aa2 = f2();,会先进入f2函数,调用一次构造函数A(int a),初始化aa。然后returnaa时,会调用拷贝构造A(const A& aa),生成一个临时变量。然后再调用一次拷贝构造A(const A& aa),将临时变量的值给aa2。出函数f2时,aa出作用域,调用析构~A()销毁。在有优化的情况下,编译器将A aa2 = f2();这段代码直接看作给aa2初始化,仅调用了一个构造函数。通过调试可以发现,编译器进入了f2函数,仅执行了一次初始化aa的构造函数,甚至都没有析构,然后就这样直接完成了aa2的初始化。
2)下面这段代码相当于把1)中的代码拆开,分成了两个步骤去写,这样写是不会触发编译器的优化的。编译器先是执行了一次构造A(int a),初始化aa1。然后又进入f2,调用构造A(int a),初始化aa。returnaa时,创建临时变量的这一步没有打印出来,但确实是存在的。之后又调用了一次赋值重载A& operator=(const A& aa),将临时变量的值给了aa1。再然后就是aa出了作用域,调用了析构~A(),至此,第二部分代码结束。
3)最后,aa1和aa2也出了作用域,再调用两次析构函数~A()


4.7再次理解类和对象


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

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

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-指短琴长-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值