一、初始化列表

之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有一种方式,就是初始化列表。

初始化列表特点:

  1. 初始化列表的使用方式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成员列表,每个"成员变量"后跟⼀个放在括号中的初始值或表达式。
  2. 每个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。
  3. 引用成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始化,否则会编译报错。
  4. C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的。
  5. 尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会走初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造会编译错误。
  6. 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持⼀致。

我们一点一点开始解释。

先看一下初始化列表的"模样"。

class Date
{
public:
	//初始化列表
	Date(int year,int month,int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{
	}
	//不使用初始化列表
	//Date(int year, int month, int day = 10)
	//{
	//	_year = year;
	//	_month = month;
	//	_day = day;
	//}

	void Print() const
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	//声明
	int _year;
	int _month;
	int _day;
};
int main()
{
	//对象定义
	Date d1(2022, 1, 1);
	d1.Print();
	return 0;
}

我们可以理解为初始化列表是每个成员变量定义初始化的地方,就如 int a = 1;一样是初始化而不是赋值。因为初始化只能一次,若重复初始化成员变量则会报错。比如:

括号中的值就是初始化成员变量的值。 当然括号中不一定全是数值,也可以是表达式,或是malloc,realloc,calloc等等。

为什么会有初始化列表呢?

因为如果成员变量被const修饰,则必须用初始化列表,打个比方,const int _n=10,直接初始化是可以的,但如果const int _n; n = 10这就不可以,初始化列表就可以充当是专门用来初始化的。

正确的写法是:

 如果成员变量是引用,则也必须写到初始化列表中去,因为引用必须要初始化否则会报错,如:int& a;这就不行。

所以,如果成员变量含有引用,必须放到初始化列表中去。

再看一段代码:

class Time
{
public :
	Time(int hour = 1)
		: _hour(hour)
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
};
class Date
{
public:
	//初始化列表
	Date(int year,int month,int day,int& xx)
		:_year(year)
		,_month(month)
		,_day(day)
		,_n(1)
		,_ref(xx)
	{
	}

	void Print() const
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	//声明
	int _year;
	int _month;
	int _day;

	const int _n; //必须在初始化列表初始化
	int& _ref;//必须在初始化列表初始化

	Time _t;//类类型成员变量
};
int main()
{
	int x = 0;
	//对象定义
	Date d1(2022, 1, 1, x);
	d1.Print();
	return 0;
}

Date类中有Time类类型成员变量_t,我们运行代码时,发现会打印出"Time()",说明Time类中的默认构造函数被调用了,经过调试,这个调用是Date类构造函数的初始化列表调用的,说明自定义成员初始化时会调用它自己的默认构造函数来进行初始化,若我们将Time类中的默认构造函数换成构造函数,就会发生错误。

 所以自定义成员变量如果没有它的默认构造函数,就会报错,解决方法就是,必须将它写到所在类的构造函数的初始化列表中来初始化。

这就是第3个特点的解释。

到这里前3个特点想必大家已经清楚了。

接下来看第4个,C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的。

先看代码:

class Date
{
public:
	Date(int year,int month,int day)
		:_year(year)
		,_month(month)
	{}
	void Print() const
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:

	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2000, 6, 6);
	d1.Print();
	return 0;
}

我们可以看到在初始化列表中没有初始化_day,所以_day就是随机值,打印结果为:

 为了应对这种问题,C++11支持在成员变量声明的位置给缺省值。请看代码:

我们可以在成员变量后给一个缺省值,如果初始化列表没有初始化成员变量,就用它的缺省值,如果初始化了,就不用。

这时打印结果为:

这里的12就是用的缺省值。

到这里先总结一下:

每个成员都要走初始化列表

1、在初始化列表初始化的成员

2、没有在初始化列表的成员

        a、声明的地方有缺省值用缺省值

        b、声明的地方没有缺省值

                b1、内置类型,不确定,看编译器,大概率是随机值

                b2、自定义类型,调用它的默认构造函数,若没有,就编译报错

3、引用、const、自定义没有默认构造函数,这三个必须在初始化列表初始化

绘制成图如下: 

对于特点6,先看一段代码:

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  1 ,理由很简单_a1一定是1,然后1在初始化_a2,所以打印结果是1 1,但不是的,这时请看第6个特点:初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持一致。

因为在A这个类中成员变量_a2在_a1前面,所以先初始化_a2,再初始化_a1。

初始_a2时_a1并不知道是多少,所以_a2是随机值,而_a1的值为1。

二、类型转换

C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。

我们先看一个简单的隐式类型转换:

int main()
{
	int i = 1;
	double d = i;
	return 0;
}

这种隐式转换想必大家都不陌生,在int类型隐式转换为double类型时,中间会产生一个临时的具有常属性的变量,大家可能不信,我们来验证一下。

但如果加上一句代码,大家看看是否正确?

int main()
{
	int i = 1;
	double d = i;

	double& rd = i;
	return 0;
}

答案是不正确的,不是因为类型不匹配,而是因为产生的中间变量具有常属性,如果这样写就会导致权限放大,在前面加上const就可以了。

int main()
{
	int i = 1;
	double d = i;

	const double& rd = i;
	return 0;
}

 好了,现在回归正题:

class A
{
public:
	A(int a = 0)
	{
		_a1 = a;
	}
	void Print()
	{
		cout << _a1 << "  " << _a2 << endl;
	}
private:
	int _a1;
	int _a2;
};

int main()
{
	A aa1(1);
	aa1.Print();

	//隐式类型转换
	A aa2 = 2; // 编译器遇到连续构造+拷贝构造->优化为直接构造
	aa2.Print();

	const A& raa2 = 2;

	return 0;
}

这段代码中A aa2 = 2 ,还能这么写?左边是A类型右边是整形,这其实就是一个隐式类型转换,只不过它比普通的复杂了一点,它是先通过构造函数生成了一个A类型的对象然后拷贝构造给aa2,只不过这里编译器优化了一下,只有构造没有拷贝构造,因为中间产生临时对象,所以也具有常性。这种情况只针对构造函数只有一个形参。

若构造函数有两个形参,也可以这样玩(C++11):

class A
{
public:
	A(int a = 0)
	{
		_a1 = a;
	}
	A(int a, int b)
	{
		_a1 = a;
		_a2 = b;
	}

	void Print()
	{
		cout << _a1 << "  " << _a2 << endl;
	}
private:
	int _a1;
	int _a2;
};


int main()
{
	A aa1(1);
	aa1.Print();
	//隐式类型转换
	A aa2 = 2; // 编译器遇到连续构造+拷贝构造->优化为直接构造
	aa2.Print();

	const A& raa = 2;

	A aa3(5, 6);
	//隐式类型转换
	A aa4 = { 5,6 };//隐式类型转换

	const A& raaa = { 9,9 };

	return 0;
}

如果我们不想支持隐式转换,在构造函数前加上explicit即可。

	//构造函数前面加explicit就不再支持隐式类型转换
	explicit A(int a = 0)
	{
		_a1 = a;
	}

 加上explicit后,就不可以隐式转换了。

同样地,类类型的对象之间也可以隐式转换,需要相应的构造函数支持。

比如:

class A
{
public:
	A(int a1)
		: _a1(a1)
	{}
	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}
int Get() const
	{
		return _a1 + _a2;
	}
private:
	int _a1 = 1;
	int _a2 = 2;
};
class B
{
public :
	B(const A& a)
		: _b(a.Get())
	{}
private:
	int _b = 0;
};
int main()
{
	A aa1 = 1;
	aa1.Print();

	//类类型的对象之间的隐式转换
	B b = aa1;
	const B& rb = aa1;

	return 0;
}

三、static成员

static成员的特点:

  1. 用static修饰的成员变量,称之为静态成员变量,静态成员变量一定要在类外进行初始化。
  2. 静态成员变量为当前类的所有对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
  3. 用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针
  4. 静态成员函数中可以访问其它的静态成员,但是不能访问非静态的,因为没有this指针。
  5. 非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
  6. 突破类域就可以访问静态成员,可以通过类名::静态成员或者对象.静态成员来访问静态成员变量和静态成员函数。
  7. 静态成员也是类的成员,受public、protected、private访问限定符的限制。
  8. 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。

不必急于看特点,先来一段代码看看:

从这段代码中可以看出,静态成员在类中是不占空间的,它其实是当前类的所有对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。

它可以在类中声明,但不可以初始化,因为初始化要走初始化列表,初始化的内容是对象中成员变量,而静态成员是当前类的所有对象所共享,不单单独属于某个对象,它不会走初始化列表,当然也不能给缺省值,所以它只能在类外面进行初始化。

将上段代码中的注释打开就会报错:

 所以类中声明的静态成员必须在类外进行初始化。

class A
{
private:
    //类内声明
	static int _a;
};
//类外初始化,可以不加static
int A::_a = 0;
int main()
{
	A aa;
	cout << sizeof(aa) << endl;
	return 0;
}

如果还不能理解,那就换一种理解方法:静态修饰的成员变量的生命周期是全局的,只不过是被限制在了类域中,访问时需要加上类域。

class A
{

//这里要改成公有才能访问
public:
	static int _a;
};
int A::_a = 0;
int main()
{
	cout << A::_a << endl;
	return 0;
}

这种方式也间接说明了它不属于某个对象。

静态成员也是类的成员,受public、protected、private访问限定符的限制。所以这里要想访问静态成员,必须先设置为公有。

如果我们偏偏就是私有,要想访问该怎么办呢?

很简单,在类中写一个静态成员函数即可。

class A
{
public:
	static int GetAa()
	{
		return _a;
	}
private:
	static int _a;
};
int A::_a = 0;

用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针

没有this指针就说明了我们可以通过类域直接访问。

int main()
{
	cout << A::GetAa() << endl;
	return 0;
}

那么,有这个东西到底有什么用处呢?

我们可以统计一个类中有多少个对象。

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()
{
	A a1, a2;
	{
		A a3(a1);
		cout << A::GetACount() << endl; //这里打印3,说明有3个对象
	}
	cout << A::GetACount() << endl; //这里打印2,说明有2个对象,a3被释放了


	cout << a1.GetACount() << endl; //我们也可以用"对象.成员函数"的方式调用
	return 0;
}

静态成员函数中可以访问其它的静态成员,但是不能访问非静态的,因为没有this指针。

因为静态成员函数中没有this指针,所以在静态成员函数中对非静态成员变量强制操作是非法的。

当然,非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。这一点想必是毋庸置疑的。

四、友元

友元提供了⼀种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类
声明的前面加friend,并且把友元声明放到⼀个类的里面,外部友元函数可访问类的私有和保护成员,友元函数仅仅是一种声明,它不是类的成员函数。

比如:

class A
{
	//友元声明
	friend void func(const A& aa);
private:
	int _a1 = 1;
	int _a2 = 2;
};

void func(const A& aa)
{
	cout << aa._a1 << endl;
}
int main()
{
	A aa;
	func(aa);
	return 0;
}

外部函数想要访问类中的私有成员可以在类中加一个友元声明。否则就会报错。

友元函数可以在类定义的任何地方声明,不受类访问限定符限制。⼀个函数可以是多个类的友元函数。就比如现实生活中我是你的朋友,我也可以是他的朋友。

//前置声明,因为在A中的友元函数声明时编译器不认识B
class B;
class A
{
	//友元声明
	friend void func(const A & aa, const B & bb);

	private:
	int _a1 = 1;
	int _a2 = 2;
};
class B
{
	// 友元声明
	friend void func(const A& aa, const B& bb);
private:
	int _b1 = 3;
	int _b2 = 4;
};
void func(const A& aa, const B& bb)
{
	cout << aa._a1 << endl;
	cout << bb._b1 << endl;
} 
int main()
{
	A aa;
	B bb;
	func(aa, bb);
	return 0;
}

必须加上前置声明,因为编译器有个原则是用任何类型变量向上找,若不前置声明,在A中的友元函数声明时编译器就不认识B,若强行访问A中的私有成员变量就会报错。

因为将自己的成员函数作为别人的友元函数不太方便,所以出现友元类。

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

代码:

class A
{
	// 友元声明
	friend class B;
private:
	int _a1 = 1;
	int _a2 = 2;
};
class B
{
public:
	void func1(const A& aa)
	{
		cout << aa._a1 << endl;
		cout << _b1 << endl;
	}
	void func2(const A& aa)
	{
		cout << aa._a2 << endl;
		cout << _b2 << endl;
	}
private:
	int _b1 = 3;
	int _b2 = 4;
};
int main()
{
	A aa;
	B bb;
	bb.func1(aa);
	bb.func1(aa);
	return 0;
}

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

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

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

五、内部类

如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,跟定义在全局想比,它只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。

比如:

class A
{
private:
	static int _k;
	int _h = 1;
public:
	class B
	{
	public:
		void foo(const A& a)
		{
			cout << _k << endl; //OK
			cout << a._h << endl; //OK
		}
	private:
		int _b = 1;
	};
};
int A::_k = 1;
int main()
{
	cout << sizeof(A) << endl;
	return 0;
}

这段代码的打印结果为4而不是8,说明了外部类定义的对象中不包含内部类

内部类默认是外部类的友元类。所以B中的foo函数能访问A中的私有成员。

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

六、匿名对象

用"类型(实参)"定义出来的对象叫做匿名对象,相比之前我们定义的"类型 对象名(实参)"定义出来的叫有名对象

代码如下:

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; //有名对象aa1
	
	//A aa1();//不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
	
	A();	//但是我们可以这么定义匿名对象,匿名对象的特点是不用取名字
	A(1);   //这也是匿名对象,匿名对象不耽误传参
	//但是匿名对象的声明周期只有它这一行,经过调试可以看到下一行它就会自动调用析构函数

	Solution s;
	cout << s.Sum_Solution(10) << endl;//有名对象这样使用

	cout << Solution().Sum_Solution(10) << endl;//匿名对象在这样场景下就很好用

	return 0;
}

匿名对象的生命周期只在当前一行,一般临时定义一个对象仅当前用一下,就可以定义匿名对象。

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

看这之前我建议大家先看一遍类和对象(中) ->C++类和对象(中),可能会更好的理解。

现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传返
回值的过程中可以省略的拷贝。

如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理。下面以vs2019为例来看看编译器的优化效果。

先来看一下自定义类型传值传参:

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()
	{
		cout << "~A()" << endl;
	}
	void Print()
	{
		cout << "A::Print()->" << _a1 << endl;
	}
private:
	int _a1 = 1;
};

void f1(A aa)
{}

int main()
{
	A aa1(1); //先构造
	f1(aa1);  //拷贝构造
	//这里没有优化,但有些更新更"激进"的编译器还会进行跨行跨表达式的合并优化
	//比如这里直接优化成一次构造

	cout << endl;

	//优化
	f1(A(1));//按理说是先构造,再传值传参进行拷贝构造,但编译器直接优化成一次拷贝构造

	cout << endl;

	//优化
	f1(1);//隐式类型转换,按理说也是先构造,再传值传参进行拷贝构造,但编译器也直接优化成一次拷贝构造

	return 0;
}

在Debug环境下的运行结果: 

再来看一下传值返回:

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()
	{
		cout << "~A()" << endl;
	}
	void Print()
	{
		cout << "A::Print()->" << _a1 << endl;
	}

private:
	int _a1 = 1;
};

A f2() //调用f2会有两次构造
{
	A aa(1); //构造
	return aa; //拷贝构造
} 

int main()
{
	f2().Print(); //调用f2()后返回临时对象,再调用Print()
	cout << "*******" << endl;
	return 0;
}

在Debug环境下的运行结果是:

我解释一下为什么打印结果是这样的,首先调用f2,定义对象aa时调用了一次构造函数,返回时又有一次拷贝构造形成一个临时对象,接着调用完毕,aa对象调用了一次析构函数,紧接着临时对象(匿名对象)调用了Print函数,再接着因为是匿名对象,直接就析构了,最后打印*******。

在Release环境下的运行结果是:

奇了怪了,这是怎么回事?这好像没有产生临时对象,你是不是在瞎编乱造呢?

不是的,这是编译器优化的结果,而且优化掉的不是临时对象,而是aa,为什么呢?

因为如果优化掉的是临时变量,那么析构应该在Print之前发生,打印结果明显不是,打印结果显示析构完后直接打印了*******,再次说明了析构的是临时对象,因为临时对象是匿名对象,匿名对象调用结束后直接析构。 这是编译器用1直接构造了一个临时对象,直接优化掉了aa。

我们再来看一段代码:

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()
	{
		cout << "~A()" << endl;
	}
	void Print()
	{
		cout << "A::Print()->" << _a1 << endl;
	}

private:
	int _a1 = 1;
};

A f2() //调用f2会有两次构造
{
	A aa(1); //构造
	return aa; //拷贝构造
} 

int main()
{
	A ret = f2();//拷贝构造
	ret.Print();
	cout << "*******" << endl;
	return 0;
}

这种形式按理说有一次构造,两次拷贝构造,根据上面之谈,编译器一定会优化的。

在Debug环境下的运行结果是:

这次应该是把临时对象给优化掉了。

首先调用f2,创建aa,调用一次构造函数,接着,拷贝构造直接给了ret,优化掉了临时对象的拷贝构造,然后,调用完后,aa被摧毁了调用了一次析构,最后ret调用了Print,程序结束ret被摧毁调用了一次析构。

为什么临时对象被优化了?

因为如果不是临时对象被优化,那么调用完Print后一定会紧接着有一个析构被调用,从打印结果上看,临时对象一定被优化了。

在Release环境下的运行结果是:

从打印结果上看,编译器的优化更"激进"了,它不但把临时对象给优化掉了,而且把aa也给优化掉了,直接用1来构造ret。 

我们再来看一段代码:

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;
	}
	void Print()
	{
		cout << "A::Print()->" << _a1 << endl;
	}

	A& operator++()
	{
		++_a1;
		return *this;
	}

private:
	int _a1 = 1;
};

A f2() //调用f2会有两次构造
{
	A aa(1); //构造
	return aa; //拷贝构造
} 

int main()
{
	A ret;
	ret = f2();//拷贝构造
	ret.Print();
	cout << "*******" << endl;
	return 0;
}

在Debug环境下的运行结果是:

这次并没有任何优化,不是所有的编译器每次都优化,而是一些可以优化的给你优化,比如连续两次拷贝构造,或者连着两次构造,像上边这样的一会构造一会拷贝构造一会复制重载,编译器在Debug下并没有优化。

在Release环境下的运行结果是:

在release环境下,优化了一点。 

这些内容大家了解一下即可。

八、总结

到这里本篇内容就结束了,类和对象也暂时完结了,希望对大家有所帮助,祝大家天天开心!

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部