再探构造函数

在类和对象的知识体系中初始化是一个重要的步骤,在前面我们主要通过构造函数进行初始化,通过在构造函数体内赋值的方式对内置函数进行初始化。

public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year; _month = month; _day = day;
	}
  • 我们通过这种方式能够解决70%初始化的问题。但是这种方式具有局限性,有三类成员是无法通过这种形式进行初始化的。

例如

  1. 自定义类
private:
    Time _t; //没有默认构造函数的类

自定义类初始化 是通过调用类的默认构造函数进行初始化的,但是如果我们定义的类 没有默认构造函数那么就无法进行初始化。

  1. 引用和const修饰的内置变量
private:
    int&_ref;    //引用
    const int _n;//const 修饰的内置变量

引用必须在定义时进行初始化,并且一旦初始化后就不能更改指向。
且引用没有默认初始化的概念,必须在构造函数初始化列表中指定绑定的变量

const变量是必须在定义是初始化,且不能被修改,无法在构造函数体内赋值因此只能在初始化列表中进行初始化。

因此我们要使用初始化列表进行初始化

  • 初始化列表的使⽤⽅式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成
    员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式。
  • C++11⽀持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显⽰在初始化列表初始化的
    成员使⽤的。
  • ————缺省值的理解

在传参时设置的数值

 Date(int year = 1, int month = 1, int day = 1)

在内置成员内初始化数值

private:
   	int _year=1; 
   	int _month=1; 
   	int _day=1;

缺省值的作用类似于初始化的备胎,当内置成员没有初始化时,缺省值可以充当初始化。当内置成员有初始化时,缺省值就没用。

  • 代码实现
class Time
{
public:
	Time(int hour)
		:_hour(hour)
	{

	}
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)
	{
	}
	void Print()const
	{
		cout << "_year" << "-" << _month << "-" << _day << 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;
}
	Date(int &x,int year = 1, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
		,_t(12)
		,_ref(x)
		,_n(1)
	{
	}
	当初始化列表中含有引用时 Date这个构造函数便不在是默认构造函数,因为参数中有了一个int &x没有赋值的变量
	,且在赋值的时候需要指定参数
	int i = 0;
	Date d1(i);
	d1.Print();

通常还有一种写法,这种写法更多

public:
	Date(int& x )
		:_year(year)
		, _month(month)
		, _day(day)
		, _t(12)
		, _ref(x)
		, _n(1)
	{
	}

	private:
		int _year=1; 
		int _month=1; 
		int _day=1;
		Time _t;     //没有默认构造的类
		int& _ref;   //引用
		const int _n;//const修饰值
};
在内置函数内设置缺省值,因为内置函数的缺省值会流向初始化列表,就相当于进行了初始化。
这样就可以省略在函数内部再次设置缺省值
  • 初始化列表的总结
    初始化列表的出现可以完全替代默认构造函数(仅仅针对于初始化),就类似默认构造函数完全可以替代Init函数一样。

尽量使用初始化列表进行初始化,因为你不在初始化列表初始化的成员也会走初始化列表。如果成员在声明为止给了缺省值,初始化列表会使用缺省值初始化,如果没有缺省值,那么就没有初始化。对于自定义成员类型,会调用默认构造函数,如果没有默认构造函数,编译器就会报错。

⽆论是否显⽰写初始化列表,每个构造函数都有初始化列表;
⽆论是否在初始化列表显⽰初始化成员变量,每个成员变量都要⾛初始化列表初始化;

  • 初始化的整个调用过程
    在这里插入图片描述
类型转换

C++支持 内置类型转化为类 类型 需要相关的构造函数
类和类之间也可以隐式转换 需要相关的构造函数

class A
{
public:
	A(int a1)
		:_a1(a1)
	{}

	A(int a1,int a2)
		:_a1(a1)
		,_a2(a2)
	{}
	int Get()const
	{
		return _a1 + _a2;
	}

private:
	int _a1 = 1; int _a2 = 2;  //内置成员设置缺省值会流向初始化列表,初始化列表中的内置函数初始化了
};

class B
{
public://公共成员只为实现一个功能  就是将A中计算结果用来初始化B
	B(const A&a)
		:_b(a.Get())
	{}
private:
	int _b = 0; //同理,内置成员设置缺省值 会流向初始化列表中的内置成员,等同于初始化
};
class A
{
public:
	//默认构造->初始化列表
	A(int a1)
		:_a1(a1)
	{

	}
	A(int a1,int a2)
		:_a1(a1)
		,_a2(a2)
	{

	}
	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; //int型的数字1 先转化为A类型的临时变量,然后用这个临时变量初始化aa1
	aa1.Print();

	const A& aa2 = 1;//int型的数字1 先转化为A类型的临时变量,然后被aa2引用


	A aa3 = { 2,2 };//两个int型的数字2 先转化为A类型的临时变量  然后用这个临时变量初始化aa3
	B b = aa3;      //A类型的aa3,先隐式转化为B类型的临时变量  然后用这个临时变量初始化b
	const B& rb = aa3;//A类型的aa3,先隐式转化为B类型的临时变量,然后被rb引用
	return 0;
}

A aa1 = 1; //int型的数字1 先转化为A类型的临时变量,然后用这个临时变量初始化aa1
const A& aa2 = 1;//int型的数字1 先转化为A类型的临时变量,然后被aa2引用
A aa3 = { 2,2 };//两个int型的数字2 先转化为A类型的临时变量 然后用这个临时变量初始化aa3
B b = aa3; //A类型的aa3,先隐式转化为B类型的临时变量 然后用这个临时变量初始化b
const B& rb = aa3;//A类型的aa3,先隐式转化为B类型的临时变量,然后被rb引用

static成员

-static成员特殊用法

1.static不能存在于对象中,它只存放在静态区,因此static初始化不通过初始化列表进行初始化。它只能通过在类外进行初始化。且初始化时一定要指明类域

int A:: _scount = 0;

2.static修饰的成员函数是没有this指针的。并且static修饰的成员函数可以访问其他static修饰的成员函数。但是不能访问非静态的。
相反非静态的成员函数,既可以互相访问。又可以访问static修饰的成员函数。

	static int GetAcout()
	{
		return _scount;
	}
	在这里面这个成员函数无法访问其他任何构造函数

3.static修饰的成员函数也是类的成员,受到public、protected、pricate访问限定符的限制。
4.突破类域可以访问静态成员,可以通过类名::静态成员。或者对象.静态成员

	cout << A::GetAcout() << endl;
	cout << a1.GetAcout() << endl;

整体代码

class A
{
public:
	A()
	{
		++_scount;
	}
	A(const A&t)
	{
		++_scount;
	}
	~A()
	{
		--_scount;
	}
	static int GetAcout()
	{
		return _scount;
	}
private:
	static int _scount;
};
int A:: _scount = 0;

int main()
{
	cout << A::GetAcout() << endl;
	A a1, a2;
	A a3(a1);
	cout << A::GetAcout() << endl;
	cout << a1.GetAcout() << endl;
	return 0;
}

友元

由于受到类访问限定符的限制,我们往往只能访问public下面公开的成员函数。而不能访问private下面的内置变量。
友元函数的出现正式为了解决这一问题。通过友元函数的声明,我们可以访问到private下的内置函数。

  • 友元的特点

1.友元是为了突破访问限定符的限制,友元分为友元函数和友元类。书写格式在前面加friend 并且吧友元放到一个类里面

例如:函数func
void func(const A& aa, const B& bb)
写成友元就在前面加上friend
friend void func(const A& aa, const B& bb)

2.友元仅仅是一种声明,它不是类里面的成员函数
3.友元可以定义在任意地方。
4.一个函数可以是多个类友元函数
5.友元类可以说是友元函数的加强版,友元类中的所有成员函数都可以是另一个类的友元函数。
6.友元类的关系是单项的,A类是B类友元,但B类不是A的友元
7.友元不具有传递性 A是B友元 B是C友元 但A不是C友元
7.友元会增加耦合度,所以友元尽量不要使用(最好的代码是高内聚,低耦合,因为这样的代码方便管理)

友元函数使用代码

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

友元类使用代码

class A
{
	friend class B;
private:
	int _a1;
	int _a2;
};

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

内部类

  • 内部类使用特点

1.一个类定义在另一个类的内部,这个内部类就叫做内部类,内部类受到外部类类域和访问限定符的限制。

例如在main函数中 想要访问A类的内部类-B
A::B b;

2.内部类默认是外部类的友元类
3.内部类本质也是一种封装,当A类跟B类紧密关联,A类实现出来就是给B类用的,如果放到private/protected的为止那么A类就只能供B类使用,其他地方用不了。

class A
{
private:
	static int _k;
	int _h = 1;
public:
	class B
	{
	public:
		void foo(const A& a)
		{
			cout << _k << endl;
			cout << a._h << endl;
		}
		int _b1;
	};
};

int A:: _k = 1; //static 函数初始化

int main()
{
	cout << sizeof(A) << endl;
	A::B b; //这里如果想访问B那就需要突破类域的限制
	A aa;
	b.foo(aa);
	return 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();
	A(1);
	A aa2(2);
	Solution().Sum_Solution(10);
	return 0;
}
运行结果
A(int a)  //这是A aa1 的构造
A(int a)   //这两个是A()的构造和析构  
~A()==
A(int a)这两个是 A(1)的构造和析构
~A()
A(int a) //这是A aa(2)的构造
~A()  //这是A aa(2)的析构
~A() //这是 A aa1 的析构

不能这么定义对象 A aa1() 因为编译器⽆法识别这是是⼀个函数声明,还是对象定义

对象拷贝时的编译器优化

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

如何优化C++标准并没有严格规定,各个编译器会根据情况⾃⾏处理。当前主流的相对新⼀点的编译器对于连续⼀个表达式步骤中的连续拷⻉会进⾏合并优化,有些更新更"激进"的编译器还会进⾏跨⾏跨表达式的合并优化。

linux下可以将下⾯代码拷⻉到test.cpp⽂件,编译时⽤g++ test.cpp -fno-elideconstructors
的⽅式关闭构造相关的优化。

优化过程图解
在这里插入图片描述

#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);
	return 0;
	//一个表达式中, 连续构造+拷贝构造->优化为一个构造
	f1(A(2));
	cout << endl;
	cout << "*********************************************" << endl;
	//传值返回
	//不优化的情况下传值返回,编译器会生成一个拷贝返回对象的临时对象作为函数表达式的返回值
	f2();
	cout << endl;
	A aa2 = f2();
	cout << endl;
	aa1 = f2();
	cout << endl;
}

更多推荐