C++ ------ 类和对象的深究

文章目录

  • 构造函数
    • 初始化列表
      • 概念
      • 特性
    • explicit关键字
  • static成员
    • 概念
    • 特点
  • 友元
    • 友元函数
    • 友元类
      • 概念
      • 特性
  • 内部类
    • 概念
    • 特点
  • 匿名对象
  • 拷贝对象时的一些编译器优化

构造函数

我们来看下面的代码:

#include 
using namespace std;

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

private:
	int _year;
	int _month;
	int _day;
};

上面的代码虽然在每次实例化对象是都给每个成员变量设定了初始值,但这并不是初始化,构造函数体内的语句只能将其称为赋初值,并不是初始化,因为初始化成员变量只能初始化一次,而构造函数可以给成员变量多次赋值,所以怎么样才能对成员变量初始化呢,这里引出了初始化列表的概念

初始化列表

概念

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

class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d(1, 1, 1);
	return 0;
}

C++ ------ 类和对象的深究_第1张图片

特性

  • 每个成员变量在初始化类表中最多只能出现一次(初始化只能初始化一次)
  • 类中包含以下成员,必须放在初始化列表位置进行初始化
  • 引用成员变量
  • const成员变量
  • 自定义类型成员(且该类没有默认构造函数时)
class A
{
public:
	A(int a)
		:_a(a)
	{
		std::cout << "A(int a)" << std::endl;
	}
private:
	int _a;
};


class B
{
	B(int a, int& b)
		:_b(a)
		,_c(b)
		,obj(2)
	{
		std::cout << "B(int a, int& b)" << std::endl;
	}
private:
	//const和引用的特点是必须在声明时初始化
	const int _b;
	int& _c;
	//自定义类型
	A obj;
};

这里需要注意:如果这里A对象为默认的无参构造函数,则B的初始化列表就不能对obj进行初始化,obj对象只能由编译器调用A类的无参的默认构造函数来创建对象。如果想要在B的初始化列表初始化我们必须把A的默认构造写成缺省构造。如以下代码:

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

class B
{
public:
	B(int a, int& b)
		:_b(a)
		, _c(b)
		,obj(10)
	{
		std::cout << "B(int a, int& b)" << std::endl;
	}
private:
	//const和引用的特点是必须在声明时初始化
	const int _b;
	int& _c;
	//自定义类型
	A obj;
};


int main()
{
	int n = 10;
	B b1(1,n);
}

当然也可以让编译器自动调用该缺省默认函数

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

class B
{
public:
	B(int a, int& b)
		:_b(a)
		, _c(b)
	{
		std::cout << "B(int a, int& b)" << std::endl;
	}
private:
	//const和引用的特点是必须在声明时初始化
	const int _b;
	int& _c;
	//自定义类型
	A obj;
};


int main()
{
	int n = 10;
	B b1(1,n);
}

C++ ------ 类和对象的深究_第2张图片

我们书写构造函数时尽量写成初始化列表的构造函数,但是初始化列表代替不了函数体内赋值,比如:

template <typename T1>
//模板类
class Stack
{
public:
	Stack(int capacity = 3)
		:_array((T1*)malloc(sizeof(T1)*capacity))
		,_capacity(capacity)
		,_top(0)
	{
		//对malloc进行判空
		if (NULL == _array)
		{
			ferror("malloc failed!");
			return;
		}
	}
	void CheckCapacity()
	{
		if (_top == _capacity)
		{
			T1* temp = (T1*)realloc(_array, sizeof(T1) * _capacity * 2);
			if (NULL == temp)
			{
				perror("realloc failed!\n");
				return;
			}
			_array = temp;
			_capacity *= 2;
			cout << "扩容成功!" << endl;
		}
	}
	void PushStack(T1 x)
	{
		CheckCapacity();
		_array[_top] = x;
		_top++;
	}
	void PopStack()
	{
		if (_top > 0)
		{
			_top--;
		}
		else
		{
			cout << "退栈失败,栈已为空!" << endl;
			return;
		}
	}
	bool EmptyStack()
	{
		return _top == 0;
	}

	T1 StackTop()
	{
		return _array[_top - 1];
	}

	~Stack()
	{
		delete[] _array;
		_array = NULL;
		_capacity = _top = 0;
	}
private:
	T1* _array;
	int _capacity;
	int _top;
//我们还可以在外面控制capacity比如用两个栈实现一个队列
class MyQueue
{
public:
	MyQueue()
	{
		
	}
	MyQueue(int capacity)
		:pushst(capacity)
		,popst(capacity)
	{}
private:
	Stack<int> pushst;
	Stack<int> popst;
};


int main()
{
	MyQueue q1(10);
}
};

这里还有一个坑,声明顺序和定义的顺序保持一致。 我们来看一下下面代码:

class A
{
public:
	A(int a)
		:_a1(a)   //_a1 = 1
		,_a2(_a1) //_a2 = 随机值
	{}

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

int main()
{
	A aa(1);
	aa.Print();
	return 0;
}

C++ ------ 类和对象的深究_第3张图片
所以我们需要在初始化列表中初始的顺序要和声明的顺序相同。

explicit关键字

构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。

class Date
{
	//友元函数
	friend ostream& operator<<(ostream& cout, const Date& d);
public:
	 Date(int year)
		:_year(year)
	{}

	/*Date(int year, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}*/
	//赋值运算符重载
	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;
};
ostream& operator<<(ostream& cout, const Date& d)
{
	cout << d._year << "-" << d._month << "-" << d._day << endl;
	return cout;
}

int main()
{
	Date d1(2022);
	//2023利用构造函数生成一个Date类型的对象然后再利用赋值运算符重载函数赋值给d1
	//但是编译器会默认优化为一个构造
	d1 = 2023;
	cout << d1 << endl;
}

单参构造函数,没有使用explicit修饰,具有类型转换的作用。
explicit 修饰构造函数,禁止类型转换
缺省构造函数,虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转换作用。explicit修饰构造函数,禁止类型转换。
以上main函数里的代码是:利用一个整型变量给日期类型对象赋值,实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值。

class Date
{
	//友元函数
	friend ostream& operator<<(ostream& cout, const Date& d);
public:
	explicit Date(int year)
		:_year(year)
	{}

	/*Date(int year, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}*/
	//赋值运算符重载
	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;
};
ostream& operator<<(ostream& cout, const Date& d)
{
	cout << d._year << "-" << d._month << "-" << d._day << endl;
	return cout;
}

int main()
{
	Date d1(2022);
	//2023利用构造函数生成一个Date类型的对象然后再利用赋值运算符重载函数赋值给d1
	//但是编译器会默认优化为一个构造
	d1 = 2023;
	cout << d1 << endl;
}

在这里插入图片描述

结论:用explicit修饰构造函数,将会禁止构造函数的隐式转换。

static成员

概念

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量,用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。
我们来看下面的代码:
实现一个类,计算程序中创建出了多少个类对象。

class A
{
public:
	A()
	{
		scount++;
	}
	A(const A& a)
	{
		scount++;
	}
	~A()
	{
		scount--;
	}
	static int GetScount()
	{
		return scount;
	}
private:
	static int scount;
};

int A::scount = 0;

A a;
int main()
{
	cout << __LINE__ << " " << A::GetScount() << endl;

	A b = a;
	cout << __LINE__ << " " << A::GetScount() << endl;
}

C++ ------ 类和对象的深究_第4张图片

特点

  • 静态成员为所有类对象所共享,不属于某个具体的对象,存放再静态区。
  • 静态成员变量必须在类外定义,定义是不添加static关键字,类中只是声明
  • 类静态成员即可用类名 :: 静态成员或者对象.静态成员来访问。
  • 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  • 静态成员也是类的成员,受public、protected、private访问限定符的限制。
    需要注意:静态成员函数可以调用静态成员函数,不能调用非静态成员函数,非静态成员函数可以调用静态成员函数。

友元

友元最基本的作用是让类外的函数可以访问类内的私有成员。但是它破化了封装性,所以我们需要谨慎使用友元,友元可以分为友元函数和友元类。
我们需要实现流插入和流提取的运算符重载,但是它们不能被实现为类成员函数,因为实现成类成员函数,会有this指针抢占第一个运算形参的位置,这就会造成冲突,this指针默认是第一个参数了。但是实际使用中cout需要是第一个形参对象才能正常使用。所以要将流插入和流提取运算符重载为全局函数。但这会导致类外的函数不能访问类内的成员,此时就需要友元来解决。看下面的代码我们实现了这两个运算符的重载。

友元函数

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)
	{}

	/*Date(int year, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}*/
	//赋值运算符重载
	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;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
	_cout << d._year << "-" << d._month << "-" << d._day << endl;
	return cout;
}
istream& operator>>(istream& _cin,Date& d) 
{
	_cin >> d._year;
	_cin >> d._month;
	_cin >> d._day;
	return cin;
}

int main()
{
	Date d1;
	//2023利用构造函数生成一个Date类型的对象然后再利用赋值运算符重载函数赋值给d1
	//但是编译器会默认优化为一个构造
	cin >> d1;
	cout << d1 << endl;
}

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

友元类

class Time
{
	//date类是Time的友元。
	//友元只能是单向的
	//在该例子中不能访问Date类中的成员而Date类可以访问Time类中的成员
	friend class Date;
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		:_hour(hour)
		,_minute(minute)
		,_second(second)
	{}
private:
	int _hour;
	int _minute;
	int _second;
};
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)
	{}

	/*Date(int year, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}*/
	//设定日期类的时间
	void SetTimeofDate(int hour, int minute, int second)
	{
		_t._hour = hour;
		_t._minute = minute;
		_t._second = second;
	}
	//赋值运算符重载
	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;
	Time _t;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
	_cout << d._year << "-" << d._month << "-" << d._day << endl;
	return cout;
}
istream& operator>>(istream& _cin, Date& d)
{
	_cin >> d._year;
	_cin >> d._month;
	_cin >> d._day;
	return cin;
}

int main()
{
	Date d1;
	//2023利用构造函数生成一个Date类型的对象然后再利用赋值运算符重载函数赋值给d1
	//但是编译器会默认优化为一个构造
	cin >> d1;
	cout << d1 << endl;
}

概念

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

特性

友元关系是单向的,不具有交换性。

  • 比如上述的Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量是不可以的,所以具有单项行,谁是谁的友元类,谁就能访问另一个类的私有成员。

友元关系不能传递

  • 如果C是B的友元,B是A的友元,则不能说明C是A的友元。

友元关系不能被继承。

内部类

class Date
{
	//友元函数
public:
	Date(int year = 1900, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}
	//Time天生就是Date类的友元。此类不占任何空间,也就是说sizeof(Date) == Date的大小
	//它受访问修饰限定符的限制,可以写成保护,私有,公共。
	class Time
	{
		friend class Date;
	public:
		Time(int hour = 0, int minute = 0, int second = 0)
			:_hour(hour)
			,_minute(minute)
			,_second(second)
		{}
	private:
		int _hour;
		int _minute;
		int _second;
		//不计算静态成员的大小
		static int k;
	};

	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 Date::Time::k = 0;
int main()
{
	Date d1;
	Date::Time t;
	//但是不能这样写
	//Time t1;
}

概念

如果一个类定义在另一个类的内部,这个类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象来访问外部类中的所有成员,但是外部类不是内部类的友元。

特点

  • 内部类可以定义在外部类的public、protected、private都是可以的。
  • 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象 /类名。
class Date
{
	//友元函数
public:
	Date(int year = 1900, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}
	//Time天生就是Date类的友元。此类不占任何空间,也就是说sizeof(Date) == Date的大小
	//它受访问修饰限定符的限制,可以写成保护,私有,公共。
	class Time
	{
		friend class Date;
	public:
		Time(int hour = 0, int minute = 0, int second = 0)
			:_hour(hour)
			,_minute(minute)
			,_second(second)
		{}
		void foo(const Date& d)
		{
			cout << k << endl;   //ok
			cout << d.i << endl; //ok
		}
	private:
		int _hour;
		int _minute;
		int _second;
		//不计算静态成员的大小
		static int k;
	};

	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;
	static int i;
};
int Date::Time::k = 0;
int Date::i = 0;
int main()
{
	Date d1;
	Date::Time t;
	t.foo(d1);
	//但是不能这样写
	//Time t1;
}

C++ ------ 类和对象的深究_第5张图片

  • sizeof(外部类) = 外部类,和内部类没有任何关系。

C++ ------ 类和对象的深究_第6张图片

匿名对象

匿名对象的特点是不用起名字,我们看下面的代码:

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

private:
	int _a;
};


int main()
{
	//匿名对象的特点是不用取名字,但是它们的生命周期只有这一行,我们证明这一点,它会在这一行结束后调用析构函数
	A();
}

C++ ------ 类和对象的深究_第7张图片
还需要注意,引用不能引用匿名对象,但是加上const以后就可以了,这是因为匿名对象就像临时变量一样具有常性,前面加上const后就可以引用了,权限可以缩小和平移不能放大。
C++ ------ 类和对象的深究_第8张图片C++ ------ 类和对象的深究_第9张图片
C++ ------ 类和对象的深究_第10张图片

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

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()
{
	// 传值传参
	A aa1;
	f1(aa1);
	cout << endl;
	// 传值返回
	f2();
	cout << endl;
	// 隐式类型,连续构造+拷贝构造->优化为直接构造
	f1(1);
	// 一个表达式中,连续构造+拷贝构造->优化为一个构造
	f1(A(2));
	cout << endl;
	// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
	A aa2 = f2();
	cout << endl;
	// 一个表达式中,连续拷贝构造+赋值重载->无法优化
	aa1 = f2();
	cout << endl;
	return 0;
}

C++ ------ 类和对象的深究_第11张图片
我们下一篇再见!

你可能感兴趣的:(C++,c++)