【类和对象(下)】

@TOC


?前言

本文章继自类和对象(中),完成收尾工作。


一、?再谈构造函数

1.1构造函数体赋值

在学习过的类和对象的基础知识中,构造函数内部通常是给成员变量一个初始值。虽然调用完构造函数后,变量有了初始值,但是不能称其为对对象的初始化,只能称其为对变量的赋值。

因为初始化只能初始化一次,而赋值可以多次赋值。

所以下面给出一个真正的初始化操作:初始化列表。

1.2初始化列表

初始化列表是构造函数的一部分,与构造函数体赋值并没有冲突,可以共存,只是初始化列表的每个变量都只能出现一次(因为初始化只初始化一次)。

初始化列表格式:

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

比如:

class Date


{









public:








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

构造函数的大括号内部仍然可以像构造函数一样写其他的东西。

注意:

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
    (1)引用成员变量
    (2)const成员变量
    (3)自定义类型成员(且该类没有默认构造函数时)

以下面的代码为例:

class B
{









public:








	class A
	{


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


	B(int a, int ref,int n)
		: _ref(ref)
		, _n(10)
	{}

private:


	A _aobj; //没有默认构造函数
	int& _ref;   // 引用
	const int _n; // const
};



不同于其他内置类型,其他内置类型可以只定义不初始化,比如:

int a;
double b;
char c;

对于引用成员变量和const成员变量来说,它们有一个共同点:
在定义的时候必须初始化。

所以在类的构造函数的初始化列表中必须有这两个成员变量的存在。

而对于自定义类型,前面我们说过,编译器自己生成的默认构造函数对内置类型不做处理,对自定义类型调用它的默认构造函数。

在初始化列表中,如果该自定义类型没有默认构造函数,(默认构造函数包括:无参构造函数,全缺省构造函数和编译器自己生成的构造函数。其中如果我们不写构造函数,编译器才会自己生成)那么在初始化列表必须要对自定义类型初始化。

如果不初始化,会报错,比如:
在这里插入图片描述
在这个例子中,A类的构造函数并不是默认构造函数,而是一个普通的构造函数,并且在B类的构造函数的初始化列表中并没有对A类进行初始化,这就会产生报错。

总结:

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

  2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
    (1)引用成员变量
    (2)const成员变量
    (3)自定义类型成员(且该类没有默认构造函数时)

  3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。

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

第四点是非常重要的一点,下面给一道题:

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
B.程序崩溃
C.编译不通过
D.输出1  随机值

这道题选择D项。首先排除程序会崩溃。
程序崩溃的原因不多,主要就是动态申请的资源泄露了或者产生死循环等等。

明显本题并没有以上情况。这道题既然能放在这里,说明该题可能会有坑,可以排除A,那就从CD之间选择。认真看代码,发现没有什么语法错误,不会编译不通过,只能选一个看似最离谱的答案D。
解析:类A实例化一个对象aa并显式地传递一个1给形参a,调用它的构造函数,此时在初始化列表中看似先初始化a1,再初始化a2,实际上是先初始化a2,再初始化a1,因为初始化的顺序取决于成员变量的声明顺序的!

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

1.3explicit关键字

explicit关键字的作用在于禁止将会禁止构造函数的隐式转换。
对于什么是隐式转换,请看下面的代码:

class Date


{









public:








	explicit Date(int year)
		:_year(year)
	{}
	//赋值运算符重载
	Date& operator=(const Date& d)
	{
		if (this != &d)
		{

			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
private:


	int _year;
	int _month;
	int _day;
};



int main()

{

	Date d2 = 2;
}

实例化的类对象中,看似是将变量2赋值给对象d2,实际上的情况是:对于不同类型的赋值,会在中间生成一个临时空间,整型2发生了隐式类型转换,升级成了类对象,这个类对象2调用构造函数,此时这块临时空间就是一个对象,然后该临时空间再调用拷贝构造函数拷贝给d2。

这个过程是:构造–>拷贝构造。

但是编译器会自动优化,优化成直接进行构造。

对于编译器的优化,我们会在下面的篇幅详细地讲到。

对于explicit关键字,就是禁止编译器对于这种隐式类型转化。

当我们加上exiplict关键字后,编译器会直接报错。在这里插入图片描述


二、?static成员

被static修饰的成员变量叫做静态成员变量,被修饰的成员函数叫做静态成员函数。普通成员变量属于对象自己所有,静态成员变量属于每个类对象所共有的。

注意:1.静态成员变量一定要在类外面进行初始化。
2.静态成员变量或静态成员函数可以直接通过类域访问。(因为它属于整个类)

对于静态成员变量, 如果设置成公有,那就跟全局变量几乎一样,都可以被类外面进行访问。

下面有一道面试题:实现一个类,计算程序中出现了多少类对象。

为了完成这道题目,我们实现的类不能在外面计算出现的类的次数。只能在类内计算,如果调用了构造函数或者拷贝构造函数,就让一个计数变量++,如果调用了析构函数,就让计数变量–。很明显需要用静态成员函数才能实现,为了获得该成员变量,我们需要在对象内实现一个静态成员函数,来跟静态成员变量进行配套使用。

静态成员函数的特点:
1.没有this指针
2.指定类域和访问限定符即可访问。

所以可以通过下面的代码来计算类的对象的个数。

class A





{









public:








	A() 
	{


		++_scount; 
	}
	A(const A& t) 
	{ 
		++_scount; 
	}
	~A() 
	{ 
		--_scount;
	}
	static int GetACount() 
	{

		return _scount;
	}
private:
	static int _scount;
};



另一道题目:
设计一个类,在类外面只能在栈/堆上创建一个对象。

class A





{









public:








	static A GetStackObj()
	{


		A a;
		return a;
	}




	static A* GetHeapObj()
	{

		return new A;
	}


private:
	A()
	{}

private:


	int _a1;
	int _a2;
};

int main()

{

	A::GetStackObj();
	A::GetHeapObj();

	return 0;
}

解析:将构造函数设置成私有的,那么所有的类型的对象都不能在类外面实例化
但是又将栈/堆的成员函数设置成静态公有的,使得我们可以在类外面直接通过类作用限定符
//直接调用该静态成员函数,达到题目要求。

再补充几点:
1.不能通过在类中给静态成员变量缺省值,因为这个地方只是静态成员变量的声明,并且缺省值实际上是给初始化列表的,对于普通成员变量来说,可以在构造函数的初始化列表中进行初始化,而静态成员变量则无法实现。
2.可以通过普通成员函数访问静态成员函数,因为对于静态成员函数来说,只要给定类域和访问限定符,就可以访问静态成员函数,在类中是不受访问限定符的限制的,也不受类域的限制,因为对于类来说这是一个整体;但不能通过静态成员函数访问普通成员函数,因为静态成员函数没有this指针,无法访问普通成员函数和成员变量。


三、?友元

友元的出现突破了类的封装机制,友元就像是类的朋友,可以随意访问类的非公有成员变量和成员函数,所以友元不适合多用。

友元分为友元函数和友元类。
友元函数:
我们知道,一般内置类型的输出可以使用

cout << 内置类型

的方式进行输出。但是对于一个自定义类型,却不能定义成类的成员函数:operator<<。因为在成员函数中有一个隐含的this指针,该指针会一直占用第一个位置,导致左操作数只能是cout。
所以如果要定义成成员函数,只能实现成 d1 << cout。(假设d1是一个日期类对象)。

如果定义成全局函数,就不再有this指针,左操作数就可以正常地中作为cout。但是重载成全局函数又导致我们无法访问到类地成员变量,这就需要友元函数的出现。

友元函数关键字:friend。
例如:

class Date


{









//友元函数的声明
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);

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



ostream& operator<<(ostream& _cout, const Date& d)
{

	_cout << d._year << "-" << d._month << "-" << d._day;
	return _cout;
}



istream& operator>>(istream& _cin, Date& d)
{
	_cin >> d._year;
	_cin >> d._month;
	_cin >> d._day;
	return _cin;
}
int main()
{
	Date d;
	cin >> d;
	cout << d << endl;
	return 0;
}

注意:

1.友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
2.友元函数可访问类的私有和保护成员,但不是类的成员函数
3.友元函数不能用const修饰
4.友元函数可以在类定义的任何地方声明,不受类访问限定符限制
5.一个函数可以是多个类的友元函数
6.友元函数的调用与普通函数的调用原理相同


四、?内部类

如果一个类定义在外部类的内部,那么这个类就叫做内部类。内部类不属于外部类,外部类不能通过任何途径访问内部类的成员。

注意:内部类天生就是外部类的友元,但外部类不是内部类的友元。

特性1:
sizeof(外部类) == 外部类,计算一个类的大小,与内部类完全没有任何关系,结果是外部类的大小。
比如:

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 a;
	cout << sizeof(a) << endl;
	return 0;
}

结果是4;

解析:对于外部类来说,内部类和外部类没有声明关系,因为内部类只是一个声明,并没有真正意义地创建内部类对象。相当于A类是一个图纸,通过A类这个图纸建造了a这所房子,但是B是A类的图纸里面的一张图纸,并没有通过B这个图纸真正创造一所房子。

特性2:
内部类受外部类的访问限定符的限制。

特性3:
建议内部类的成员变量声明在外部类,这样内部类既可以使用它的成员变量,外部类也可以使用内部类的成员变量。
例题:求和


class Solution {
public:








    int Sum_Solution(int n) 
    {
        Sum a[n];
        return _sum;       
    }


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




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



五、?匿名对象

请看下面的代码以及注释:

class A





{









public:








	A(int a = 0)

	:_a(a)

	{

	cout << "A(int a)" << endl;
	}


	

	~A()
	{

	cout << "~A()" << endl;
	}

private:

	int _a;
};



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

};


int main()
{
	A aa1;
// 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
//  A aa1();
// 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,
// 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
	A();
	A aa2(2);

//不能这样调用:
	//Solution.Sum_Solution(10);
	//Solution::.Sum_Solution(10);
	//对于第1种情况,不能直接使用类型调用它的成员函数
	//对于第二种情况,只有static成员函数或者成员变量可以这样调用,因为静态成员函数没有this指针,而普通成员函数需要传递this指针,如果这样调用,就无法传递this指针。
	Solution().Sum_Solution(10);
	return 0;
}

匿名对象特性:

1.匿名对象的生命周期在当前行
有名对象的生命周期在当前的局部作用域
2.匿名对象类似于临时对象,都具有常性
3const引用延长了匿名对象的生命周期

1.匿名对象的生命周期只有1行
	A(2);
2.匿名对象具有常性,如果这样定义结果是权限放大了,不行
	A& pa = A(3);
	const A& ra = A(3);//这样就可以,const修饰的变量具有常性,这样是权限平移。
3.const引用修饰的匿名对象延长了它的生命周期,使其生命周期延长到当前函数作用域。

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

在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝。

几种优化情况:
1.同一行一个表达式中连续的构造+构造会优化成为1个构造。
2.同一行一个表达式中连续的构造+拷贝构造会优化成为1个构造
3.不同行的构造+赋值重载不会优化

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

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

	// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
	A aa2 = f2();

	// 一个表达式中,连续拷贝构造+赋值重载->无法优化
	A aa2;
	aa2 = f2();

	return 0;
}

七、?再谈类和对象

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


?总结

类和对象收尾工作到此结束。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MY6DqXsO' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片