C++类和对象(下)

一、初始化列表

1.之前我们实现构造函数时,初始化成员变量主要使⽤函数体内赋值,构造函数初始化还有⼀种⽅
式,就是初始化列表,初始化列表的使⽤⽅式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成
员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式。
class A
{
public:
	 A(int a1)
		:_a1(a1)
	{}
private:
	int _a1;
};

这就是经典的初始化列表,初始化列表紧跟在默认构造函数之后,形式比较奇怪:主要通过 : 、, 和 ()实现初始化。

每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是每个成员变量定义 初始化的地⽅。

2.引⽤成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进⾏初始化,否则会编译报错。

#include<iostream>
using namespace std;
class Time
{
public:
	Time(int hour)
		:_hour(hour)
	{
		cout << "Time()" << endl;
		cout << "" <<_hour<< endl;
	}
private:
	int _hour;
};
class Date
{
public:
	Date(int& x, int year=1, int month=1, int day=1)
		:_year(year)
		, _month(month)
		, _day(day)
		//, _t(12)
		//, _ref(x)
		//, _n(1)
	{
		// error C2512: “Time”: 没有合适的默认构造函数可⽤
		// error C2530 : “Date::_ref” : 必须初始化引⽤
		// error C2789 : “Date::_n” : 必须初始化常量限定类型的对象
		_t(12);
		_ref(x);
		_n(1);
	}
	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
		cout << "" << _n << endl;
		cout << "" << _ref << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t; // 没有默认构造
	int& _ref; // 引⽤
	const int _n; // const
};
int main()
{
	int i = 0;
	Date d1(i);
	d1.Print();
	return 0;
}

我们来看上面的代码,成员变量_t没有默认构造函数,_ret是引用,_n是const类型变量,这三种类型的变量如果不在初始化列表中定义的话,就会引发编译报错。

 所以引⽤成员变量,const成员变量,没有默认构造的类类型变量这三种类型的变量一定要在初始化列表中定义。

3.C++11⽀持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显⽰在初始化列表初始化的成员使⽤的。

private:
	int _year=2024;
	int _month=7;
	int _day=23;
	Time _t; // 没有默认构造
	int& _ref; // 引⽤
	const int _n; // const

我们在成员变量声明的位置给一个值就是该成员变量的缺省值,如果我们没有将_year、_month和_day这三个成员变量初始化的话,这三个成员变量就会使用缺省值。

我们来看下面的代码:

#include<iostream>
using namespace std;
class Time
{
public:
	Time(int hour)
		:_hour(hour)
	{
		cout << "Time()" << endl;
		cout << "" <<_hour<< endl;
	}
private:
	int _hour;
};
class Date
{
public:
	Date(int& x, int year=1, int month=1, int day=1)
		//:_year(year)
		//, _month(month)
		//, _day(day)
		: _t(12)
		, _ref(x)
		, _n(1)
	{
		// error C2512: “Time”: 没有合适的默认构造函数可⽤
		// error C2530 : “Date::_ref” : 必须初始化引⽤
		// error C2789 : “Date::_n” : 必须初始化常量限定类型的对象
	}
	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
		cout << "" << _n << endl;
		cout << "" << _ref << endl;
	}
private:
	int _year=2024;
	int _month=7;
	int _day=23;
	Time _t; // 没有默认构造
	int& _ref; // 引⽤
	const int _n; // const
};
int main()
{
	int i = 0;
	Date d1(i);
	d1.Print();
	return 0;
}

我们在初始化列表中将 _year、_month和_day这三个成员变量的初始化屏蔽了,这时我们打印年月日就会发现年月日都变为了缺省值。

相比较与使用构造函数进行初始化,我们更加推荐使用初始化列表进行初始化,因为那些你不在初始化列表初始化的成员也会⾛初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会⽤这个缺省值初始化。如果你没有给缺省值,对于没有显⽰在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没显⽰在初始化列表初始化的⾃定义类型成员会调⽤这个成员类型的默认构造函数,如果没有默认构造会编译错误。

4.初始化列表中按照成员变量在类中声明顺序进⾏初始化,跟成员在初始化列表出现的的先后顺序⽆关。建议声明顺序和初始化列表顺序保持⼀致。

 我们来看下列代码:

#include<iostream>
using namespace std;
class A
{
public:
	A(int a)
		:_a1(a)
		, _a2(_a1)
	{}
	void Print() {
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2=2;
	int _a1=2;
};
int main()
{
	A aa(1);
	aa.Print();
}

大家觉得打印出来的结果会是多少呢?打印出来的结果是1和一个随机值,因为初始化列表中按照成员变量在类中声明顺序进⾏初始化,_a2先声明,所以先初始化_a2,用_a1初始化_a2,此时_a1还未被a初始化,所以_a1也是随机值,所以_a2是随机值,_a1被a初始化所以是1。

 二、类型转换

1.C++⽀持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
类型转换就是编译器在看到赋值双方类型不匹配时,会将创建一个同类型的临时变量,将  = 右边的值拷贝给临时变量,再拷贝给  = 左边的值。
就好像你将一个double类型的变量赋值给一个int类型的变量,就会将double类型变量的整数部分先拷贝到一个int类型的临时变量,再将临时变量的值赋给int类型的变量。

而C++支持内置类型转换为类类型,我们来看下面的代码:

#include<iostream>
using namespace std;
class A
{
public:
	 A(int a1)
		:_a1(a1)
	 {
		 cout << "A(int a1)" << endl;
	 }
	 A(const A& a)
	 {
		 _a1 = a._a1;

		 cout << "A(const A& a)" << endl;
	 }
	void Print()
	{
		cout << _a1 << " "  << endl;
	}
private:
	int _a1 = 1;
};
int main()
{
	// 1构造⼀个A的临时对象,再⽤这个临时对象拷⻉构造aa3
	// 编译器遇到连续构造+拷⻉构造->优化为直接构造
	A aa1 = 1 ;
    aa1.Print();
	return 0;
}

我们可以看到,结果只调用了一次构造函数,如果像上面所说的类型转换一样,不应该还要调用拷贝构造函数吗?这其实是编译器的优化,当编译器遇到连续构造+拷⻉构造时就会优化为直接构造。

2.构造函数前⾯加explicit就不再⽀持隐式类型转换 

#include<iostream>
using namespace std;
class A
{
public:
	// 构造函数explicit就不再⽀持隐式类型转换
	// explicit A(int a1)
	 explicit A(int a1)
		:_a1(a1)
	 {
		 cout << "A(int a1)" << endl;
	 }
	 A(const A& a)
	 {
		 _a1 = a._a1;

		 cout << "A(const A& a)" << endl;
	 }
	void Print()
	{
		cout << _a1 << " "  << endl;
	}
private:
	int _a1 = 1;
};
int main()
{
	// 1构造⼀个A的临时对象,再⽤这个临时对象拷⻉构造aa3
	// 编译器遇到连续构造+拷⻉构造->优化为直接构造
	A aa1 = 1 ;
    aa1.Print();
	return 0;
}

我们在构造函数前加上explicit,这时编译器就会报错。

 如果我们不想使用隐式类型转换,只需在构造函数前加上explicit。

三、static成员

static成员的特点:

⽤static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进⾏初始化。
1.  静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
2. ⽤static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。
3. 静态成员函数中可以访问其他的静态成员,但是不能访问⾮静态的,因为没有this指针。
4. ⾮静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
5. 突破类域就可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。
6. 静态成员也是类的成员,受public、protected、private 访问限定符的限制。
7. 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不⾛构造函数初始化列表。
利用静态成员变量的特点,我们可以计算出程序中创建了多少个类对象。
#include<iostream>
using namespace std;
class A
{
public:
	A()
	{
		++_scount;
	}
	A(const A& t)
	{
		++_scount;
	}
	~A()
	{
		--_scount;
	}
	static int GetACount()
	{
		return _scount;
	}
private:
	// 类⾥⾯声明
	static int _scount;
};
// 类外⾯初始化
int A::_scount = 0;
int main()
{
	cout << A::GetACount() << endl;
	A a1, a2;
	A a3(a1);
	cout << A::GetACount() << endl;
	cout << a1.GetACount() << endl;
	// 编译报错:error C2248: “A::_scount”: ⽆法访问 private 成员(在“A”类中声明)
	//cout << A::_scount << endl;
	return 0;
}

这便是静态成员的一种基本的用法。

结果:

四、友元

 友元提供了⼀种突破类访问限定符封装的⽅式,友元分为:友元函数和友元类,在函数声明或者类声明的前⾯加friend,并且把友元声明放到⼀个类的⾥⾯。

1.友元函数

外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数。
我们来看下面的代码:
#include<iostream>
using namespace std;

class Date
{
	//friend void Print(const Date& d);
public:
	Date (int year=2024,int month=7,int day=23)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};

void Print(const Date& d)
{
	cout << "" <<d._year << "-" <<d. _month << "-" << d._day;
}

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

因为成员变量为私有的,类外函数并不能访问类内的私有成员,此时会报错。

 我们只要将类外函数声明为友元函数,这时类外函数就能够正常访问类的私有成员。

友元函数的特点:

1.友元声明可以写在类中的任意位置,不受类访问限定符限制。

2.⼀个函数可以是多个类的友元函数。

2.友元类

友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。

我们来看下面的代码:

#include<iostream>
using namespace std;

class Time
{
	friend class Date;
public:
	Time(int hour=8,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=2024,int month=7,int day=23)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
	void Print(const Time& t)
	{
		cout << "" << _year << "-" << _month << "-" << _day << endl;
		cout << "" << t._hour << "-" << t._minute << "-" << t._second << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};



int main()
{
	Date d1;
	Time t1;
	d1.Print(t1);
	return 0;
}

上面的代码将Date类声明为Time类的友元类,所以Date类中的成员函数都是Time类的友元函数,所以在Print函数中可以访问到Time类中的私有成员。

结果:

友元类的特点:

1.友元类的关系是单向的,不具有交换性,⽐如A类是B类的友元,但是B类不是A类的友元。

2.友元类关系不能传递,如果A是B的友元, B是C的友元,但是A不是B的友元。

这里要注意的是,虽然友元有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多⽤。

五、内部类

如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。
我们来看下面的代码:
#include<iostream>
using namespace std;

class A
{
public:
	class B
	{
	public:
		B(int b=1)
			:_b(b)
		{}
		void Print(const A& a)
		{
			cout << "" << a._a << endl;
			cout << "" << _b << endl;
		}
	private:
		int _b;
	};
	A(int a=2)
		:_a(a)
	{}
private:
	int _a;
};

int main()
{
	A a1;
	A::B b1;
	b1.Print(a1);
	return 0;
}

在上面的代码中我们将类B定义为类A的内部类,因为内部类默认是外部类的友元类,所以类B是类A的友元类,B中的成员函数能够访问类A的私有成员。

结果:

内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使⽤,那么可以考
虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其
他地⽅都⽤不了。

内部类的特点:

1.内部类和其外类是独立存在的,计算外类的大小时,是不包括内部类的大小的。

2.内部类受访问限定符的限定,如果为私有则无法直接被使用。

3.内部类天生就算外类的友元,即内部类可以访问外类的成员,而外类无法访问内部类。

六、匿名对象

⽤ 类型(实参) 定义出来的对象叫做匿名对象,相⽐之前我们定义的 类型 对象名(实参) 定义出来的
叫有名对象。
匿名对象⽣命周期只在当前⼀⾏,⼀般临时定义⼀个对象当前⽤⼀下即可,就可以定义匿名对象。
我们来看下面的代码:
#include<iostream>
using namespace std;

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

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

我们定义了一个匿名对象,通过判断构造函数和析构函数的执行来判断匿名对象的生命周期。

结果:

从图中我们可以看出,执行完构造函数后便执行了析构函数,所以匿名对象的生命周期只在当前一行。 

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

现代编译器会为了尽可能提⾼程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传参
过程中可以省略的拷⻉。
我们来看下面的代码:
#include<iostream>
using namespace std;
class A
{
public:
	A(int a = 0)
		:_a1(a)
	{
		cout << "A(int a)" << endl;
	}
	A(const A& aa)
		:_a1(aa._a1)
	{
		cout << "A(const A& aa)" << endl;
	}
	A& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a1 = aa._a1;
		}
		return *this;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a1 = 1;
};
void f1(A aa)
{}
A f2()
{
	A aa;
	return aa;
}
int main()
{
	// 传值传参
	A aa1;
	f1(aa1);
	cout << endl;
	// 隐式类型,连续构造+拷⻉构造->优化为直接构造
	f1(1);
	// ⼀个表达式中,连续构造+拷⻉构造->优化为⼀个构造
	f1(A(2));
	cout << endl;
	 return 0;
}

从结果中我们可以看出,在传值传参中会调用拷贝构造函数,而其他两种方法都会存在连续的构造和拷贝构造,所以编译器就将他们优化为一个构造。

八、总结

以上就是我对于类和对象(下)的理解,希望以上所讲能够对你有所帮助,有帮助的话记得一键三连哦,感谢各位。

  • 15
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值