初始化列表、静态成员、友元

1. 再谈构造函数

1.1 构造函数体赋值 vs 初始化列表

  • 构造函数体赋值:构造体函数中的语句将其成为赋值不能称为初始化,因为初始化只能初始化一次,构造体函数内可多次赋值。
  • 初始化列表:以一个冒号开始,用逗号分隔的数据成员列表,每个成员变量后跟一个放在括号中的初始值或表达式。
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. 构造函数体内是赋值操作(可多次赋值)
  3. 在执行构造函数时,先走初始化列表,再走函数体(在实践中,尽可能使用初始化列表初始化,不方便的再使用函数体)。

1.2 必须使用初始化列表的成员

  • 引用成员、const成员、无默认构造函数的自定义类型必须在成员列表中初始化。
typedef int DataType;
class Stack
{
private:
	int* _array;
	int _capacity;
	int _size;
public:
	Stack(int capacity)//这里需要传参,不是默认构造函数
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (_array == NULL)
		{
			perror("_array malloc fail");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}

	void Push(DataType data)
	{
		if (_capacity == _size)
		{
			int newcapacity = 2 * _capacity;
			DataType* p = (DataType*)realloc(_array, newcapacity * sizeof(DataType));
			_capacity = newcapacity;
			_array = p;
		}
		_array[_size] = data;
		_size++;
	}
};

class MyQueue
{
private:
	Stack _pushst;
	Stack _popst;
	int _size;
	const int _n;
	int& _ref;
    int* _ptr;
public:
	MyQueue(int n, int& rr)
		: _pushst(n)//显示调用Stack的构造函数,初始化_pushst
		, _popst(n)
		, _size(0)
		, _n(10)
		, _ref(rr)
        ,_ptr((int*)malloc(40))
	{
        memset(_ptr, 1, 40);//这里要先给空间,在初始化空间中的值设置为1,此时初始化列表不能全部完成,需要借助函数体才能实现
	}
};

int main()
{
	int xx = 0;
	MyQueue q1(10, xx);

	return 0;
}

在上述程序中

  • private中只是声明,不开辟空间。
  • 初始化列表:本质可以理解为每个对象中成员定义的地方。
  • Stack(自定义类型)没有默认构造,在定义_pushst和_popst时需要传参初始化,这就需要MyQueue去显式的写默认构造,在初始化列表中初始化自定义类型,若放在函数体中初始化,会报错“没有合适的默认构造函数可用”。
  • const成员必须在定义时初始化,且只有一次初始化机会即只能初始化一次,如果在函数体中对const赋值是不合适的(函数体中可以多次赋值),所以const需要在初始化列表中初始化。
  • 引用成员变量一定引用一个实体,再不能引用其他实体,必须在定义时初始化,所以也需要在初始化列表中初始化。

1.3 成员初始化顺序

  • 规则:按成员声明顺序初始化(与初始化列表顺序无关),初始化顺序尽量和声明顺序一致。  

class A
 {
 public:
 A(int a)
        :_a1(a)
        ,_a2(_a1)
    {}
 void Print() {
 cout<<_a1<<" "<<_a2<

上述程序的输出为 “1 随机值”,因为先声明a2再声明a1,在初始化列表中a2先初始化=》a2(a1) = 随机值, a1再初始化a1(a)==a1(1)==1.

  • 对于自定义类型成员变量,会先使用初始化列表初始化:初始化列表无论写不写,每个成员变量都会默认在初始化列表上走一遍,此时,自定义类型在这调用自己的默认构造函数(没有默认构造函数汇报错),内置类型有缺省值就调用,没有就取决于编译器是否处理。

1.4 隐式类型转换

class A
{
public:
	// 单参数构造函数
	//explicit A(int a)
	A(int a)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}

	// 多参数构造函数
	A(int a1, int a2)
		:_a(0)
		,_a1(a1)
		,_a2(a2)
	{cout << "A(int a1, int a1)" << endl;}
	A(const A& aa)
		:_a(aa._a)
	{
		cout << "A(const A& aa)" << endl;
	}

private:
	int _a;
	int _a1;
	int _a2;
};

int main()
{
	A aa1(1);//输出A(int a)
	// 拷贝构造
	A aa2 = aa1;//输出A(const A& aa)

	// 隐式类型转换:内置类型转换为自定义类型
	// 3构造一个A的临时对象,在用这个临时对象拷贝构造aa3,
    //编译器遇到连续构造+拷贝构造->优化为直接构造
	A aa3 = 3;//这里优化为直接构造,输出A(int a)

	// raa 引用的是类型转换中用3构造的临时对象 
	const A& raa = 3;//因为临时对象具有常性,需要在引用前加const作修饰

	
    //多参数
	A aaa1(1, 2);
	A aaa2 = { 1, 2 };
	const A& aaa3 = { 1, 2 };

	return 0;
 }
  • 隐式类型转换:将内置类型转换成自定义类型。

在A aa3 = 3;中,语法上,3会首先构造一个A的临时对象(A(int a)),再将这个临时对象拷贝构造aa3(A(const A& aa)),但当编译器遇到构造+拷贝构造时,会优化为直接构造(A(int a))。

const A& raa = 3;中,3构造一个临时对象,临时对象具有常性,引用时需要添加const修饰。

  • 为什么类型转换要产生临时对象?

类型转换若不产生临时对象,则内置类型3不能直接构造自定义类型A。因为A支持用int去构造,所以可以用int类型去构造,同时3是内置类型,不能直接去构造自定义类型,所以需要用3去构造一个临时的自定义类型,再用这个临时的自定义类型去拷贝构造。

  • 内置类型怎么能转换成自定义类型呢?

是由单参数构造函数支持的,多参数不支持。

2. Static成员

  • 声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量
  • 用 static修饰的成员函数,称之为静态成员函数
  • 静态成员变量一定要在类外进行初始化

特性与使用场景

class A {
public:
    A() { ++_scount; }
    static int GetCount() { return _scount; }
private:
    static int _scount; // 声明
};
  • 静态成员为所有对象共享,不属于某个具体的对象(内存位于静态区)
  • 必须类外初始化int A::_scount = 0;
  • 类静态成员可用 类名::静态成员 或者 对象.静态成员来访问
  • 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  • 静态成员也是类的成员,受public、protected、private访问限定符的限制

3. 友元

友元提供了一种突破封装的方式,有时提供便利,但会增加耦合度,破坏封装,不易多用。

分类:友元函数、友元对象

3.1 友元函数

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

friend ostream& operator<<(ostream& _cout, const Date& d);

 特性

  • 可访问私有成员,但非成员函数
  • 不能用const修饰
  • 可以定义在任何地方,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数
  • 友元函数的调用与普通函数调用原理相同

3.2 友元类

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

特性:

  • 单向关系,不具有交换性:DateTime的友元类 → Date可访问Time私有成员,反之不可
  • 不可传递性,C是B的友元,B是A的友元,不能说明C是A的友元
  • 友元关系不能继承

4. 编译器优化

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

类:

class A
{
private:
	int _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;
	}
};
  • 函数--传值传参:
void f1(A aa)//2.自定义类型传参,A aa(aa1),调用拷贝构造,用aa1初始化aa,输出"A(const A& aa)"
{}//3.结束时,aa销毁,自动调用析构,输出"~A()" 

int main()
{
	//传值传参
	A aa1;//1.生成时自动调用默认构造,输出"A(int a)" 
	f1(aa1);//4.函数结束时调用析构,输出"~A()"

}
  • 函数--传值返回:存在连续的拷贝构造return aa,和拷贝构造A ret = f2(),优化为aa直接构造ret,同时aa的析构与ret的析构也优化为一个析构
A f2()
{
	A aa;//1.构造
	return aa;//2.传值返回,拷贝构造
}//3.析构

int main()
{
	A ret = f2();//4.拷贝构造ret
	return 0;//5.析构
}
  • 函数--隐式类型:隐式类型转换涉及构造+拷贝构造会优化为直接构造
  • 特别的,在一个表达式中,连续构造+拷贝构造会优化为一个构造
  • 一个表达式中,连续的拷贝构造+赋值重载,无法优化
A f2()
{
	A aa;//构造
	return aa;//传值返回,拷贝构造
}

void f1(A aa)//临时对象拷贝,调用拷贝构造
{}

int main()
{
	f1(1);//A支持用int行构造,所以用1先构造一个临时对象,调用构造。构造+拷贝构造
    f1(A(2));//A(2)构造一个对象,再拷贝给aa,构造+拷贝构造
    A aa1;
    aa1 = f2();//拷贝构造+赋值
	return 0;

}

你可能感兴趣的:(好好学sei,java,开发语言)