类和对象(超详细版)

发布时间:2024年01月11日

目录

1.类引入

2.类定义

3.2类作用域

3.类的访问限定和封装

?3.1类访问限定

4.类的实例化和大小

4.1实例化

4.2类对象大小

5.this指针

6.类的默认函数

6.1构造函数

?6.2析构函数

6.3拷贝构造函数

6.4.赋值运算符重载

?6.4.1运算符重载

6.4.2赋值运算符重载

6.4.3前置++和后置++

7.日期类的实现

7.1>>和<<重载

8.const

9.取地址和const取地址重载

10构造函数的更多内容

10.1explicit关键字

11.static成员

12.友元

?12.1友元函数

12.2友元类

13.内部类

14.编译器的一些优化


1.类引入

在c++中,结构体struct被认为是类的一种,

1.类名就是类型

2.可以在类里面定义函数

struct Stack
{
	Stack* a;
	void a()
	{

	}
};

2.类定义

class NN//NN是类名,class是定义类的关键字
{
	//类的主体,包含类的成员
	//变量称为类的属性或成员变量
	//函数称为类的方法或成员函数
};

class NN
{
	void a(int h)
	{
		//...
	}
	int c;
	int k;
};
这样写,里面的函数,会被编译器认为是内联函数

第二种,可以把声明留在类里,定义放在外面或别的文件
比如类的声明在.h文件,定义在.c文件
.h文件
class NN
{
	void a(int h);

	int c;
	int k;
};
.c文件
void NN::a(int h)
{
	//...
}
注意,这里要类名::
因为要用作用域限定符来指定函数究竟是哪个作用域的

注意,不要出现函数的形参和类里别的变量名或函数名相等的情况,容易出错
也容易混淆,一般对要加前缀或后缀区分,在函数中,如果没有指定作用域,那么编译器对于作用域,优先搜索
局部,再去全局变量或静态变量里找,有指定优先找指定

两种方法可以混用,在类里面定义的函数,虽然被认为是内联,但同样的
编译器也可以选择忽略
因此,长的函数定义放在外面,短的函数直接写在类里面

3.2类作用域

::是作用域限定符,{}也是定义一个作用域,

for(int i=0;i<10;i++)
{
    int a;
}
cout<<a;
我们这时候就会编译错误,因为a是在{}所规定的局部域里面
定义的,在外面就不能用了

3.类的访问限定和封装

?3.1类访问限定

c++封装的方式,就是通过类将对象的属性和方法放在一起,通过访问权限,选择性的让外部用户调用接口(方法或对象)

访问限定分为:public公有,protected(保护),private(私有)
注意:
1.public修饰的成员,可以在外面直接被调用

2.protected和private是不能在外面直接调用的(在这里pritected和private是一样的)

//注意,public在外面和类里面都能调用

3.访问限定符的作用域,直到遇到下一个访问限定符

4.如果没有遇到下一个,那就遇到类的}结束

5.class默认都是private,struct默认都是public(兼容c)

注意限定符是在我们写代码的时候限定我们调用的,在编译器编译之后,没有区别

class NN
{
public:
	void a(int h)
	{
		//...
	}
private:
	int c;
	int k;
};

4.类的实例化和大小

4.1实例化

类本身就像是个设计图,在没有实例化前,是不占空间的

比如

class NN
{
public:
	void a(int h)
	{
		//...
	}
private:
	int c;
	int k;
};


int main()
{
	NN c;
	c.a(3);
	return 0;
}
必须先NN c先,才能对c这个实例化后的对象,进行访问其内部成员

4.2类对象大小

计算类大小的时候,不计算成员函数的大小,因为函数是放在函数表里的,所以在类里面是通过地址找函数的,所以一个类大小,只计算成员变量.

如果这个类是空(没有成员变量)的,没有成员,那就是1个字节,不存数据,就标识有这个对象存在过

计算类大小的时候可以参考结构体的内存对齐规则,具体可以看我结构体的文章
?

5.this指针

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

int main()
{
 Date d1, d2;
 d1.Init(2022,1,11);
 d2.Init(2022, 1, 12);
 d1.Print();
 d2.Print();
 return 0;
}
明明调用的是同一个函数,为什么结果会不一样呢,因为c++引入了this指针
编译器会自动把上面的内容这样翻译:
 void Print(Date *const this)
 {
 cout <<this->_year<< "-" <<this->_month << "-"<<this-> _day <<endl;
 }
 d1.Print(&d1);
 d2.Print(&d2);
注意,是编译器可以这样翻译,我们只能理解,不能直接这样写
形参和实参不能显示的使用this,只能在成员函数里面使用,比如上面cout的内容
就是合法的。

6.类的默认函数

如果一个类,什么成员都没有,那么就称为空类,但事实上,并不是完全没有成员,类默认会有6个成员函数,构造函数、析构函数、拷贝构造、赋值重载、取地址重载(主要是普通对象和const对象取地址,很少会自己实现)

6.2构造函数

6.1构造函数

?构造函数是一个特殊的成员函数,名字与类相同,创建类类型对象时,编译器自动调佣,保证每个数据成员有合适的初始值,在对象的一个生命周期内只调用一次。简单来说,就是自动初始化对象的。

1.函数名与类名相同;

2.无返回值;

3.对象实例化时编译器自动调用对应的构造函数

4.构造函数可以重载

class NN
{
public:
	NN() {};//无参构造函数
	NN(int c, int k)//带参构造函数
	{
		_c = c;
		_k = k;
	}

private:
	int _c;
	int _k;
};


int main()
{
	NN b;//调用无参构造,注意不能加(),否则就是函数声明了
	NN c(3, 4);//调用带参构造函数
	return 0;
}

5.如果类里面没有定义显示的定义构造函数(就是我们自己写构造函数),那编译器会自动生成一个无参的默认构造函数,一旦我们显示定义了构造函数,那就不会生成。

如果我们只定义了带参构造函数,调用却是以无参构造函数的形式调用,那么就会报错

所以一般,我们用构造函数,都是定义一个带参(全缺省参数),这样就不会出事了,注意,不要让全缺省构造函数和无参构造函数同时出现,虽然构成了函数重载,但会出现歧义,还是会报错。


?

6.编译器的默认构造函数,也是要分情况的。假如,我们用两个栈实现队列,这个时候,我们以无参的形式实例化队列,编译器会自动调用栈的默认构造函数(可能你自己定义了,也可能没定义让编译器自己生成)来初始化栈,但对于内置类型,也就是int\char\long long 等,理论上也会调用默认构造函数,但是这个默认构造函数什么都不干。(当然,如果你栈也是让编译器自己生成,那也是什么都不干)

注意,对于内置类型的默认构造函数,具体还是要看具体的编译器,vs2019有时候就会自动初始化,vs2013就不会。(注意,并不是说内置类型是个函数,而是说默认构造函数这个机制对于内置类型的处理,规定上是不处理,但一些编译器自己会优化)

7.针对上面的问题,c++11允许,内置类型成员变量可以在声明时就给默认值,但注意这还是声明,因为是放在类里的,也就是自定义类型,那就是个设计图,没有开辟实际空间,就还不是定义。

class NN
{
public:
	NN() {};
	NN(int c, int k)
	{
		_c = c;
		_k = k;
	}
	

private:
	int _c = 1;
	int _k;
};

综上,一般情况下,我们都是自己写构造函数,但如果成员都是自定义类型(并且这些自定义类型我们都手动写了构造函数),可以让编译器自己生成,如果有内置类型,且在声明时就给了默认值,那也可以考虑让编译器自己生成默认构造函数。

8.默认构造函数只能有一个(无参构造函数、全缺省构造函数、我们自己不写,编译自己生成的构造函数,这3个之一,同时出现会报错)。

?6.2析构函数

注意,整个实例化的类对象的销毁,是由编译器完成,那么对于对象里面的成员,如何销毁呢,如果只是成员变量或成员函数没什么,但如果是顺序表、栈这些复杂的东西,里面的空间是需要手动销毁的。

这个时候,对象在销毁时,会自动的调用析构函数,然后将对象中这些资源(空间等)的清理工作。

class NN
{
public:
	NN() {};
	NN(int c, int k)
	{
		_c = c;
		_k = k;
	}
	~NN()
	{
		_c = 0;
		_k = 1;
	}

private:
	int _c = 1;
	int _k;
};


int main()
{
	NN b;
	NN c(3, 4);
	return 0;
}

1.析构函数名是类名前+~。

2.无参数无返回值类型

3.一个类只能有一个析构函数。若未显示定义,系统会自动生成默认的析构函数,注意,析构函数不能重载

4.对象生命周期结束时(要被销毁了),编译器会自动调用

5.默认生成的析构函数,对于内置类型,不做处理,也没必要处理,因为程序结束,操作系统会自动回收这些内置类型成员变量的空间的(类对象在实例化之后,对象的变量也是开辟在栈等空间上的,这些空间都是操作系统开辟的,回收也是由操作系统回收)。

对于自定义类型,则是会调用该自定义类型的默认析构函数(如果这个自定义类型没有我们主动写析构函数,那调用的也是该自定义类型的默认析构函数,只是大概率没效果)

举个例子,还是用两个栈实现队列,队列类的对象,编译器会自动生成一个对该队列的析构函数,队列的析构函数,会调用栈的析构函数,从而把两个栈都销毁掉(如果栈类的析构函数是我们自己写的话),因为没有别的自定义类型了,队列的析构函数就完成了任务。

6.3拷贝构造函数

如果已经存在该类的对象了,创建同类类型的新对象时,编译器会自动调用拷贝构造函数

?1.拷贝构造函数是构造函数的一个重载形式,一般得加const,以免拷贝错了

	NN(const NN &a)
	{
		//...
	}

2.拷贝构造函数的参数只有1个,且必须是该类类型对象的引用(指针也行,但c++规定是引用),因为如果用传值传参,这个时候,拷贝构造函数的形参是接受来自实参的值传递,又相当于调用了一次新的拷贝构造函数,如此,无穷尽也,就会出事,所以,编译器会直接报错。

3.编译器自动生成的拷贝构造函数,对于内置类型,是值拷贝(浅拷贝),或者说,按内存顺序直接拷贝过去,对于自定义类型,会调用该类型的拷贝构造函数,为什么要独特处理呢。

注意,在c++中如果用值拷贝自定义类型,会出事,假如是栈(数据结构),那么用传值调用,由于析构函数在对象生命周期结束后,会自动调用,这个时候,形参和实参都会对空间进行一次释放。

因此对于涉及资源申请(例如空间开辟)的类,我们应当自己写拷贝构造函数,对于不涉及的,可以让编译器自己生成。(注意,还有一种类型,就是我们进行嵌套,比如用两个栈实现队列,这个时候队列的拷贝构造我们没必要写,因为对于自定义类型,会调用它的拷贝构造,所以我们只需要把栈的拷贝构造写好就行)

4.针对浅拷贝的问题,我们可以采用深拷贝,深拷贝就是开辟一样大的空间,一样的值,还是栈的问题,我们可以在拷贝构造函数里面,开辟跟被拷贝的栈一样大的空间,再把值拷贝到这个新开辟的空间,这样析构函数就算被调用了,也是各自释放各自的空间,也就不会出事了。

5.对于,拷贝构造,一般用在:用已经存在的同类的对象初始化或拷贝。

函数参数为类类型对象或返回值为类类型对象。

已存在类型,和函数参数前面说过了,这边主要是说返回值的问题,我们知道,传值返回的时候,因为函数调用结束,空间会被销毁,空间里的东西也都变成非法的,要创建临时变量来存储返回值,这个行为,是将类类型对象拷贝到临时变量,也要调用一次拷贝构造函数,之后把临时变量拷贝给接受的对象,那又会调用一次拷贝构造函数,所以对于返回值,我们可以采用引用返回的方式。

6.4.赋值运算符重载

?6.4.1运算符重载

为什么要有运算符重载呢,因为我们有了类,这时候可能会需要比较,但问题是,编译器也不知道我们比较的规则等,所以编译器对于自定义类型,干脆是不提供比较方法,要我们自己想办法,对于内置类型,编译器最开始就知道怎么比,所以有运算符直接提供给我们用。

这些运算符可以直接转换为机器能识别的指令。

格式:返回值类型 operator操作符(参数列表)

bool operator> (NN x, NN y)
{
	if (x._c > y._c)return true;
	else return false;
}

int main()
{
	NN B1, B2;
	cout << (B1 > B2);
//这个等同于cout<<(operator>(B1,B2));
	return 0;
}

但这样我们忽略了封装问题,成员变量一般都是利用,如果不采用全局的operator,我们可以封装成成员函数。

class NN
{
public:
	bool operator> (NN y)//可以是引用也可以传值
	{
		if (_c > y._c)return true;
		else return false;
	}
   //对于成员函数来说,可以随意调用同类的成员,不受限制
//对于成员函数来说,一直有个默认的this指针,作为第一参数,
//所以只需要一个参数即可.
private:
	int _c = 1;
	int _k;
};




int main()
{
	NN B1, B2;
	cout << (B1 > B2);
    //B1>B2在编译器视角下,是NN::operator>(&B1,B2);
	return 0;
}

运算符重载注意事项:

不能连接其他符号来创建新的操作符:比如operator$;

重载操作符必须有一个类类型参数

用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义

作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数第一个参数为隐形的this指针。

.*? ? ::? ? sizeof? ? ?:? ? .? ?这5个不能重载

示例

class NN
{
public:
	NN(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	int getday(int year, int month1)
	{
		int month[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
		if (month1 == 2 && (year % 4 == 0 && year % 100 != 0 || year % 400 == 0))
		{
			return 29;
		}
		return month[month1];
	}
//判断当前月份天数
	NN& operator+= (int day)
	{
		_day += day;
		while (_day > getday(_year, _month))
		{
			_day -= getday(_year, _month);
			_month++;
			if (_month > 12)
			{
				_year++;
				_month = 1;
			}
		}
		return *this;
	}
    //得出当前天数

	NN operator+ (int day)
	{
		NN T(*this);
		T._day += day;
		/*while (_day > getday(_year, _month))
		{
			_day -= getday(_year, _month);
			_month++;
			if (_month > 12)
			{
				_year++;
				_month = 1;
			}
		}*/
//前面已经有+=的操作了,我们这里可以省略,直接调用就行
		return T;
	}
//+的话,不应该改变自身,所以用拷贝函数创建一个新的对象。

	void print()
	{
		cout << _year << '/' << _month << '/' << _day;
	}
private:
	int _year;
	int _month;
	int _day;
};




int main()
{
	NN d1(2024, 1, 4);
	NN d2 =d1 + 100;
	 d2.print();
	 d1.print();
	return 0;
}

6.4.2赋值运算符重载

格式:参数类型:const 类型名&,传引用提高效率

返回值类型:类型名&,返回引用提高效率,有返回值,是为了能够直接拿来赋值

检测自己是否给自己赋值

返回*this:返回值

示例

#include<iostream>
using namespace std;

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

	NN& operator=(const NN& a)
	{
		if (this != &a)
		{
			_year = a._year;
			_month = a._month;
			_day = a._day;
		}
		return *this;
	}

	
	void print()
	{
		cout << _year << '/' << _month << '/' << _day;
	}
private:
	int _year;
	int _month;
	int _day;
};




int main()
{
	NN d1(2024, 1, 4);
	NN d2 =d1 + 100;
	 d2.print();
	 d1.print();
	return 0;
}

1.赋值运算符只能重载成类的成员函数,不能重载成全局函数,因为编译器会自动生产一个赋值运算符的重载在类里面(如果你不显示定义),这样全局和类里面会冲突,所以不能重载全局。

2.编译器默认生成的赋值运算符,跟拷贝构造很像,对内置类型的变量,直接采用逐字节值覆盖,自定义类型采用该类型的赋值运算符。

3.同样,针对涉及资源管理(在堆上开辟空间等行为),最好是自己写赋值运算符,不涉及的,可以让编译器自己生成。

6.4.3前置++和后置++

	NN& operator++()
	{
		_day++;
		return *this;
	}
前置++,当前类的day++,返回this指针
	NN operator++(int)
	{
		NN tmp(*this);
		_day++;
		return tmp;
	}
为了区分前置和后置,c++规定,后置的里面参数要加个int,不用我们传,编译器自动传
    

7.日期类的实现

?date.h

#pragma once
#include<iostream>
using namespace std;

class NN
{
public:
	NN(int year=1, int month=1, int day=1);
	int getday(int year, int month1);

	NN& operator+= (int day);
	NN operator+ (int day);
	NN& operator=(const NN& a);
	NN& operator-= (int day);
	NN operator- (int day);
	NN& operator++();
	NN operator++(int);
	NN& operator--();
	NN operator--(int);

	bool operator==(const NN& a);
	bool operator>(const NN& a);
	bool operator<(const NN& a);
	bool operator>=(const NN& a);
	bool operator<=(const NN& a);
	bool operator!=(const NN& a);
	int operator-(const NN& a);
	void print();
private:
	int _year;
	int _month;
	int _day;
};

date.cpp

#include"date.h"

NN::NN(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day; 
	if (_year < 1 || _month>12 || _day < 1 || _day>getday(_year, _month))
	{
		this->print();
		cout << "日期非法" << endl;
	}
}

int NN::getday(int year, int month1)
{
	int month[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	if (month1 == 2 && (year % 4 == 0 && year % 100 != 0 || year % 400 == 0))
	{
		return 29;
	}
	return month[month1];
}
NN& NN::operator+= (int day)
{
	if (day < 0)
	{
		return *this -= (-day);
	}
	_day += day;
	while (_day > getday(_year, _month))
	{
		_day -= getday(_year, _month);
		_month++;
		if (_month > 12)
		{
			_year++;
			_month = 1;
		}
	}
	//* this = *this + day;
	return *this;
}
NN NN::operator+ (int day)
{
	NN T(*this);
	T += day;
	//T._day += day;
	//while (T._day > getday(T._year, T._month))
	//{
	//	T._day -= getday(T._year, T._month);
	//	T._month++;
	//	if (T._month > 12)
	//	{
	//		T._year++;
	//		T._month = 1;
	//	}
	//}
	return T;
}
NN& NN::operator=(const NN& a)
{
	if (this != &a)
	{
		_year = a._year;
		_month = a._month;
		_day = a._day;
	}
	return *this;
}

NN& NN::operator++()
{
	*this += 1;
	return *this;
}
NN NN::operator++(int)
{
	NN tmp(*this);
	*this += 1;
	return tmp;
}

bool NN::operator==(const NN& a)
{
	if (_year == a._year && _month == a._month && _day == a._day)
	{
		return true;
	}
	else
	{
		return false;
	}
}
bool NN::operator>(const NN& a)
{
	if (_year > a._year)
	{
		return true;
	}
	else if (_year == a._year && _month > a._month)
	{
		return true;
	}
	else if (_year == a._year && _month == a._month && _day > a._day)
	{
		return true;
	}
	else return false;
}
void NN::print()
{
	cout << _year << '/' << _month << '/' << _day;
}

bool NN::operator>=(const NN& a)
{
	return *this > a || *this == a;
}
bool NN::operator<(const NN& a)
{
	return !(*this >= a);
}

bool NN::operator<=(const NN& a)
{
	return !(*this > a);
}
bool NN::operator!=(const NN& a)
{
	return !(*this == a);
}


NN& NN::operator-= (int day)
{
	if (day < 0)
	{
		return *this += (-day);
	}
	_day -= day;
	while (_day <= 0)
	{
		--_month;
		if (_month == 0)
		{
			--_year;
			_month = 12;
		}
		_day += getday(_year, _month);
	}
	return *this;
}
NN NN::operator- (int day)
{
	NN a(*this);
	a -= day;
	return a;
}

int NN::operator-(const NN& a)
{
	int flag = 1;
	NN max(*this);
	NN min(a);
	if (*this < a)
	{
		max = a;
		min = *this;
		flag = -1;
	}
	int d = 0;
	while (min != max)
	{
		++min;
		d++;
	}
	return d * flag;

}

NN& NN::operator--()
{
	*this -= 1;
	return *this;
}
NN NN::operator--(int)
{
	NN tmp(*this);
	*this -= 1;
	return tmp;
}

7.1>>和<<重载

注意,我们打印自定义类型不能直接用cout,为什么(我们之前知道cout和cin是自动识别类型,这是因为c++自己写了内置类型的函数重载,从而识别)cout是自ostream类里面的,cin是在istream类里面的。如果我们要用cout和cin输入输出指定的自定义类型,我们要运算符重载。

class NN
{
public:
	NN(int year=1, int month=1, int day=1);
	int getday(int year, int month1);

	NN& operator+= (int day);
	NN operator+ (int day);
	NN& operator=(const NN& a);
	NN& operator-= (int day);
	NN operator- (int day);
	NN& operator++();
	NN operator++(int);
	NN& operator--();
	NN operator--(int);

	bool operator==(const NN& a);
	bool operator>(const NN& a);
	bool operator<(const NN& a);
	bool operator>=(const NN& a);
	bool operator<=(const NN& a);
	bool operator!=(const NN& a);
	int operator-(const NN& a);
	void print();
	friend ostream& operator<<(ostream& out, const NN& d);
	friend istream& operator>>(istream& in,  NN& d);
这是友元,就是为了让外面的函数可以调用类私有的成员
private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& out, const NN& d);
istream& operator>>(istream& in, NN& d);
ostream& operator<<(ostream& out, const NN & d)
{
	//这是流提取的运算符重载
	//注意,我们不采用成员函数的形式重载运算符,而是全局
	//因为,双目操作符的规定,第一个参数一定是左操作数,第二个参数是右操作数
	//如果我们用成员函数的形式重载运算符,则会出现d1<<cout这样别扭的形式
	//为了遵从习惯,我们采用全局的方式,
	//这样才能出现cout<<d1这样的形式。
	// 注意,cout是个函数,需要被传参数,实现效果,不能
	//加const,但要输出的类对象,我们可以用const修饰
	//为了实现连续操作比如cout<<d1<<d2,我们要给函数一个返回值,返回cout,从而
	//让cout继续做新一轮的左操作数,<<这个操作符的结合性是从左边开始的,赋值
	//操作符的结合性是从右开始。out是cout的引用,返回out的引用,本质还是返回cout的引用,不影响
	out << d._year << d._month << d._day << endl;
	return out;
}

istream& operator>>(istream& in, NN& d)
{
	//跟<<差不多,区别是cin是在istream类里,且这次是流插入,也就是把东西
	//插入类对象里面,所以类对象形参不能加const。
	in >> d._year >> d._month >> d._day;
	return in;
}

注意,重载>>和<<,一定是重载了流插入和流提取。因为位运算,一般是针对整型这样的内置类型。

8.const

void print() const
当这个成员函数,采用const修饰
当调用时实际上是这样翻译的
void print(const NN*const this)
const NN d1;
d1.print()
翻译:d1.print(&d1)

如果不加const,则是print(NN *const this)
这样我们传d1(用const NN d1),就会出现权限放大。

注意,如果我们用非const调用const修饰的print
不会报错,因为权限可以缩小。
因此,const对象和非const对象都可以调用const修饰的成员函数
但,const对象不能调用没有const修饰的成员函数

注意,为了保持参数匹配,事实上我们前面日期类函数,当有const对象参与,有可能会出问题,比如d1(const)<d2(非const),参数不匹配,所以,我们定义成员函数的时候,对于不会修改内容的函数,尽量都加入const修饰,会修改内容的成员函数,不要加const修饰。

9.取地址和const取地址重载

?注意,const修饰的函数,如果有重名,本身也构成函数重载,因为接受的参数类型不一样

	NN* operator&()
	{
		return this;
		
	}
	const NN* operator&()const
	{
		return this;
	}

注意,这两个重载,一般不会自己写,除非你想让别人获取到你指定的内容
自己不写,编译器自己会写。

这里我们要注意,编译器虽然会翻译我们的指令,但是不是像我在注释里写的那样,把我们的代码改掉,而是直接将代码翻译成正确的汇编指令,因此这里调用 重载的&在汇编指令的角度,不会造成死循环。具体可以看看汇编指令,这里不多说。

10构造函数的更多内容

const类型的变量和引用类型的变量,都不能直接通过在构造函数里面初始化。

这里我们要讲明的是,类本身是个图纸,里面的成员变量只是声明,定义是在实例化类,也就是创建了类对象的时候。

而类对象里面的成员定义,是在初始化列表里面的,而const和引用这两类必须在定义的时候就初始化,所以,这两类必须在初始化列表里面定义并初始化。

而对于一般的成员变量(一般的内置类型),虽然在初始化列表里定义,但值是编译器给的随机值,所以我们有必要在构造函数里面给这些变量初始化。如果是自定义类型,会调用它的初始化列表进行定义,再调用它的构造函数.

class aa
{
public:
	aa(int a = 1)
	{
		h = a;
	}
private:
	int h;
};


class NN
{
public:
	NN(int year = 1, int month = 1, int day = 1)
		:a(year)
		,c(_month)
	{
		_month = month;
		_year = year;
		_day = day;
	}
	
private:
	const int a;
	int& c;
	int _year;
	int _month;
	int _day;
    aa h;
};

对于这个代码,自定义类型aa的h对象,会调用它的默认构造,而默认构造包含初始化列表和变量初始化。
因为这里的默认构造是缺省类型参数,所以编译器会自动初始化h
但如果是
aa(int a )
	{
		h = a;
	}
那么,就必须
NN(int year = 1, int month = 1, int day = 1)
		:a(year)
		,c(_month)
        ,h(2)
	{
		_month = month;
		_year = year;
		_day = day;
	}
来初始化

另外,对于一般的内置类型,如果我们给了缺省值,那么这个缺省值就是在初始化列表的时候就赋值给了变量。如果对于这样类型的变量,我们主动在初始化列表里赋值,那么就会经过(赋值缺省值-赋值给定值),如果是没有缺省值的,但我们给值,((这个不是初始化列表里初始化语句做的,可以结合下面的关于声明顺序的代码看)赋值随机值,赋值给定值),

借助初始化列表,如果遇到一个自定义类型a里面全是自定义类型b,且b类型的构造函数是一般参数,而不是全缺省参数,这时候,我们可以在a类型里面写构造函数,在初始化列表的部分初始化类型b。

总结下:

1.初始化只能初始化1次,因此一个成员在初始化列表只能出现一次

2.const类型成员变量,引用成员变量,自定义类型成员(注意,没有默认构造函数,主要针对是构造函数没有缺省值,必须我们主动给值)

3.尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。

class aa
{
public:
	aa(int a)
	{
		h = a;
	}
private:
	int h;
};


class NN
{
public:
	NN(int year = 1, int month = 1, int day = 1,int n)
		:a(year)
		,c(_month)
        ,h(n)
	{
		_month = month;
		_year = year;
		_day = day;
	}
	
private:
	const int a;
	int& c;
	int _year;
	int _month;
	int _day;
    aa h;
};

但不能全靠初始化列表搞定,比如判断空间开辟是否失败,以及memset置空等操作。

4.成员变量的初始化列表的初始化顺序,跟我们在里面写的先后顺序无关,而是与在声明时的顺序有关

class A
{
public:
    A(int a)
        :_a1(a)
        ,_a2(_a1)
    {
        
    }
    void Print() {
        cout<<_a1<<" "<<_a2<<endl;
    }
private:
    int _a2;
    int _a1;
};
int main() {
    A aa(1);
    aa.Print();
}
结果是1和随机值

为了防止我们有时候糊涂了,尽量声明顺序和初始化列表顺序一致

10.1explicit关键字

class NN
{
public:
	NN(int year = 1)
	{
		_year = year;
	}
private:

	int _year;

};

int main()
{
	NN a(3);
	NN b = 4;
}
4是个内置类型的
但赋值要匹配类型,所以这时候会出现隐式类型转换
这时候会创建一个临时变量NN c(4)
再用拷贝构造函数,把c拷贝给b

这样操作的前提是NN这个类的构造函数是单参数类型或者第一个参数没有
缺省,其他都是缺省值,又或者全缺省

注意,这样的话,浮点和整型都可以直接隐式转成类型NN
但像指针,就不行了,因为这里的隐式转换,本身还是将常量的类型匹配构造函数的第一个参数
指针无法隐式转int,所以NN b=0x100000是错的
但如果重载构造函数,
NN(int *a)
{
    /....
}
就可以了


class NN
{
public:
	NN(int year , int month = 1, int day = 1)
	{
		_month = month;
		_year = year;
		_day = day;
		c++;
	}
	NN(const NN& a)
	{
		_month = a._month;
		_year = a._year;
		_day = a._day;
		c++;
	}
	static int getc()
	{
		return c;
	}
	void h()
	{
		cout << getc();
	}
private:

	int _year;
    int _month;
    int _day;
	static int c;
};
int main()
{
    NN d1(2023,12,12)
    NN d2=(2023,12,12);
注意,这样两者表达是不同的,第一个是构造函数
第二个是NN d2=12,是逗号表达式。
如果是c++98是不支持多参数直接隐式转换的,但c++11支持
NN d2={2023,12,12};
const NN&d={2023,12,12};
}

如果不想支持常量隐式转换成类型

explicit NN(int year = 1)
	{
		_year = year;
	}
这样就能拒绝隐式转换,当然强转还是不能拒绝的

11.static成员

?类成员在声明时,有static修饰的,称为类的静态成员,静态成员变量,静态成员函数,静态成员变量一定要在类外面初始化。

静态成员变量不支持缺省值,因为定义和初始化不走初始化列表

1.静态成员为所有类对象共享,不属于具体哪个对象,放在静态区。

2.静态成员变量必须在类外定义,定义时不加static关键字,类里面只是声明

3.静态成员函数没有this指针,不能访问非静态成员

4.静态成员也是类的成员,收public、protected、private访问限定符的限制。

class NN
{
public:
	NN(int year = 1, int month = 1, int day = 1)
	{
		_month = month;
		_year = year;
		_day = day;
		c++;
	}
	NN(const NN& a)
	{
		_month = a._month;
		_year = a._year;
		_day = a._day;
		c++;
	}
	int getc()
	{
		return c;
	}
private:

	int _year;
	int _month;
	int _day;
	static int c;
};
int NN::c = 0;
int main()
{
	//NN g;
	//g.c=1   这里会报错,因为c是私有,改成公有就可以
	//NN::c = 1;    如果是c是公有可以
	//针对私有:
	//1种,为了调用而创建一个对象,并且加一个成员函数,返回c即可
	//NN g
	//g.getc()-1
	//创建一个匿名对象
	//NN().getc()-1
	//匿名对象生命周期只有这一行
	//之所以-1,是因为这里c是用来计算类对象在创建时经过多少次
	//构造和拷贝构造

}
class NN
{
public:
	NN(int year = 1, int month = 1, int day = 1)
	{
		_month = month;
		_year = year;
		_day = day;
		c++;
	}
	NN(const NN& a)
	{
		_month = a._month;
		_year = a._year;
		_day = a._day;
		c++;
	}
	static int getc()
	{
		return c;
	}
private:

	int _year;
	int _month;
	int _day;
	static int c;
};
int NN::c = 0;
int main()
{
//此时getc函数是静态成员函数,不需要this指针
因此没有必要创建一个类对象
//直接用域限定符,就可以了.
//注意,编译器查找,需要指向区域。
	cout<<NN::getc() - 1;
	cout << NN().getc() - 1;
	NN a;
	cout << a.getc() - 2;
}

静态成员函数,不可以调用非静态成员,因为没有this指针,无法指定是哪个对象的

非静态成员函数可以调用静态成员,因为有this指针,可以指定对象。

12.友元

?12.1友元函数

class NN
{
public:
	NN(int year = 1, int month = 1, int day = 1);
	int getday(int year, int month1);

	NN& operator+= (int day);
	NN operator+ (int day);
	NN& operator=(const NN& a);
	NN& operator-= (int day);
	NN operator- (int day);
	NN& operator++();
	NN operator++(int);
	NN& operator--();
	NN operator--(int);

	bool operator==(const NN& a);
	bool operator>(const NN& a);
	bool operator<(const NN& a);
	bool operator>=(const NN& a);
	bool operator<=(const NN& a);
	bool operator!=(const NN& a);
	int operator-(const NN& a);
	void print();
	friend ostream& operator<<(ostream& out, const NN& d);
	friend istream& operator>>(istream& in, NN& d);
private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& out, const NN& d);
istream& operator>>(istream& in, NN& d);
ostream& operator<<(ostream& out, const NN& d)
{

	out << d._year << d._month << d._day << endl;
	return out;
}

istream& operator>>(istream& in, NN& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}

友元的关键字是friend

1.友元函数可以访问类的私有和保护成员,但不是类的成员函数

2.友元函数不支持const修饰

3.友元函数可以在类定义的任何地方声明,不受类访问限定符限制
4.一个函数可以是多个类的友元函数
5.友元函数的调用与普通函数的调用原理相同

12.2友元类

class NN
{
	friend OO;
public:
	NN(int year = 1)
	{
		_year = year;

	}


private:

	int _year;

};
class OO
{
public:
	OO()
	{

	}
	int get()
	{
		c._year = 10;
	}
private:
	int a;
	NN c;
};
int main()
{

}

1.友元关系是单向的,不具有交换性。
上面代码,OO类里面可以访问NN类对象的私有成员,但NN类里面不能访问OO类的私有成员
2.友元关系不能传递
如果C是B的友元, B是A的友元,则不能说明C时A的友元。
3.友元关系不能继承。

13.内部类

?

#define _CRT_SECURE_NO_WARNINGS 1
#include "test.h"

class OO
{
public:
	OO()
	{

	}
	class NN
	{
	public:
		NN(int year = 1)
		{
			_year = year;

		}


	private:

		int _year;

	};
private:
	int a;

};
class LL
{
public:
	LL(int year = 1)
	{
		_year = year;

	}


private:

	int _year;

};
class KK
{
public:
	KK()
	{

	}
	
private:
	int a;
	LL o;
};
int main()
{
	cout<<sizeof(OO);
//答案是4,因为本质上没有定义NN类对象
	cout<<sizeof(KK);
//答案是8,因为KK类的声明里面有一个LL类对象
	OO l;
	OO::NN u;
//内部类在本质上还是一个类,比如NN类受OO类的域和限定符限制
//NN类本身天生就是OO类的友元
}

如果一个类定义在另一个类的内部,这个类就叫内部类。内部类是独立的雷,不属于外部类,不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。

内部类就是外部类的友元类,内部类可以通过外部类的的对象参数,来访问外部类中的所有成员,但外部列不是内部列的友元。

1.内部类可以定义在外部类的public、protected、private都是可以的。

2.注意内部类可以直接访问外部类的static成员,不需要外部类的对象/类名

3.sizeof(外部类)=外部类

?14.匿名对象

class NN
{
public:
	NN(int year = 1, int month = 1, int day = 1)
	{
		_month = month;
		_year = year;
		_day = day;
		c++;
	}
	NN(const NN& a)
	{
		_month = a._month;
		_year = a._year;
		_day = a._day;
		c++;
	}
	int getc()
	{
		return c;
	}
private:

	int _year;
	int _month;
	int _day;
	static int c;
};
int NN::c = 0;
int main()
{
	//创建一个匿名对象
	NN().getc()-1
	//匿名对象生命周期只有这一行
	//之所以-1,是因为这里c是用来计算类对象在创建时经过多少次
	//构造和拷贝构造
匿名对象不用取名,生命周期只有一行,结束后自动调用析构函数
}

14.编译器的一些优化

?

#define _CRT_SECURE_NO_WARNINGS 1
#include "test.h"

class NN
{
public:
	NN(int b=1)
	{
		a = b;
	}
private:
	int a;

};

int main()
{
	NN a(1);
	NN b = a;//这是拷贝构造,因为b是还未定义的,a是已经定义的
	NN c(2);
	c = a;//赋值拷贝,因为两者都是已经初始化过的,这里是赋值。

	//按照c++的语法,按理来说NN c=1,要先构造临时变量,再用临时变量拷贝构造c
	//但编译器对于这个行为,进行了优化,在实际上只会进行一次构造。
	//在同一个表达式中,有些行为会被合二为一。构造+构造-》构造
	//构造+拷贝构造-》构造
	//拷贝构造+拷贝构造-》拷贝构造
	//上面是一般编译器都会做的优化,但一些编译器会非常激进,把中间不必要的东西直接优化了

}

?

文章来源:https://blog.csdn.net/MXZSL/article/details/135342952
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。