C++入门篇4---类和对象 (下)

一、构造函数的补充

1.初始化列表

我们先看下面这段代码

class A
{
private:
	int _a;
	const int _b;
	int& _c;
public:
	A(int a,int b,int& c)
	{
		_a=a;
        _b=d;
        _c=c;
	}
};

这段代码有问题吗?咋一看,确实没什么问题,该赋值的都赋值了,但是我们在仔细看一下这些被赋了初值的成员变量,除了_a,其他两个成员变量按照规定,应该在定义的时候就应该被初始化了,而构造函数的函数体内是赋初值的地方,换句话说,这些变量在进入构造函数体之前就已经开辟好空间了,那么问题是哪里是定义这些成员变量的地方呢?

这里就要引入初始化列表这一概念,专门用来定义成员变量的地方,即所有的成员变量都会先在这里定义开辟空间,这里说明一下,初始化列表无论写不写,编辑器都会走,因为成员变量需要定义开辟空间,不写相当于只定义不给初始值,函数体内只进行赋初值的操作

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

class A
{
private:
	int a;
	const int b;
	int& c;
public:
	A(int a,int b,int& c)
		:_a(a)
		,_b(b)
		,_c(c)
	{
        //...
    }
};

注意:

1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
1)引用成员变量
2)const成员变量
3)自定义类型成员(且该类没有默认构造函数时)
,举个例子

class A
{
private:
	int _a;
public:
	A(int a)//非默认构造函数
	{
		_a = a;
	}
};
class B
{
private:
	A a;
	int b;
public:
	B(int a,int b)
	:a(a)//可以调用构造函数
	,b(b)
	{
		//...
	}
};

(这里就能解释为什么不写初始化列表,自定义成员只能调用默认构造函数,因为没有给参数,编辑器只能调用默认构造函数)

3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,
一定会先使用初始化列表初始化
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();
}

C++入门篇4---类和对象 (下)_第1张图片

很多人估计会觉得打印1 1,但是请大家看仔细了,我们先声明的是_a2,然后是_a1,所以初始化列表的顺序是先走_a2(_a1),很显然这时候_a1没有初始化,是随机值,所以_a2是随机值,再走_a1(a),_a1被初始化为1

还有一点补充:其实类的成员变量是可以给缺省值的(如果初始化列表没有写,那么会将成员变量定义为缺省值,注意:先走的初始化列表,然后再走的构造函数,如果一个成员变量在初始化列表和构造函数里都被赋值了,那么它的最终值就是构造函数中被赋的值)

class A
{
public:
	A()
	{}
private:
    int _a=1;
	int _b=2;
	int _c=3;
};

2.explicit关键字

下面介绍另一种调用构造函数的方法

class Date
{
private:
	int _year;
	int _month;
	int _day;
public:
	Date(int year)
		:_year(year)
		,_month(1)
		,_day(1)
	{
		cout << "Date(int year)" << endl;
	}
    //拷贝构造是构造函数的函数重载,本质也是构造函数的一种,所以也有初始化列表
	Date(const Date& d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)
	{
		cout << "Date(const Date&d)" << endl;
	}
};

int main()
{
	Date a(1);//正确
	Date b = 1;//正确
	return 0;
}

上面代码的两种方法其实都是调用了构造函数,但是其实底层的原理稍有区别,第一种就是单纯的调用构造函数,第二个本质其实是发生了隐式类型转换,先调用构造函数创建了一个临时的Date类型的变量,再拷贝构造给b,但是编辑器将这个过程优化成了直接调用构造函数,如下

C++入门篇4---类和对象 (下)_第2张图片

 那么如果我们不想让构造函数的调用发生类型转换怎么办?这里就要介绍explicit关键字了,用explicit修饰的构造函数就不能用第二种方法构造

explicit Date(int year)
	:_year(year)
	,_month(1)
	,_day(1)
{
	cout << "Date(int year)" << endl;
}

当有多个参数的构造函数时,可以用{}调用

Date(int year,int month)
	:_year(year)
	,_month(month)
	,_day(1)
{
	cout << "Date(int year)" << endl;
}

Date x={2023,8};

二、static成员

1.静态成员变量

class A
{
private:
	int _a;
	int _b;
	static int _c;
public:
	A(int a=1,int b=1)
		:_a(a)
		,_b(b)
	{}
};

int A::_c = 1;

第一个问题:A这个类的大小是多少?

答案是8,因为静态成员变量在静态区,不处于某个单一的对象,而是处于这个类的所有的对象

第二个问题:既然静态成员函数不属于某个单一的对象,那么构造函数能对静态成员初始化吗?

答案是不能,因为构造函数仅仅只能初始化对象的成员变量,而静态成员变量处于所有这个类的对象,如果每个对象创建时都会对静态成员初始化,这是不符合静态变量的定义的,静态变量只能被初始化一次

第三个问题:那么静态成员变量在哪里初始化?

很显然,上面的代码已经给出了答案,静态成员变量在类外初始化

2.静态成员函数

与正常的成员函数不同,静态成员函数没有this指针,也就是说静态成员函数不能访问该对象(即调用这个函数的对象)里面的非静态成员,(但是可以将该对象当作参数传给静态成员函数,这样就能访问非静态成员变量了),当然这样也有一个好处:我们可以用  类名::函数  的方式直接调用静态成员函数

class A
{
private:
	int _a;
	int _b;
	static int _c;
public:
	A(int a=1,int b=1)
		:_a(a)
		,_b(b)
	{}
    static void Print()
    {
        cout << _c << endl;//只能访问静态成员变量
    }
	static void Print(const A&d)//当作参数传进来,可以访问
	{
		cout << d._a << " " << d._b <<" "<< _c << endl;
	}
};

int A::_c = 1;
int main()
{
	A a;
	a.Print();
	a.Print(a);
    A::Print();
    A::Print(a);
	return 0;
}

总结:

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


三、友元

1.友元函数

之前我在类和对象(中)介绍了运算符重载,那C++的输入输出其实就是<<和>>两个操作符的重载实现的,那么如果我们要实现一个日期类的输出,怎么写?(先看代码和结果,待会会解释代码)

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

ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << '/' << d._month << '/' << d._day << endl;
	return out;
}

int main()
{
    Date x(2023,8,2);
    Date y(2023,8,3);
    cout<

C++入门篇4---类和对象 (下)_第3张图片 

注意:cout属于ostream(输出流),cin属于istream(输入流)

函数解释:<<操作符有两个操作数,第一个是输出流,即cout,第二个是要输出的对象,这里就是Date,所以该函数有两个参数,至于这个返回值是为了防止出现连续打印的情况,就如上面的cout<

问个问题:上面的代码有什么可以改进的吗?或者说有什么不足?

其实我们在仔细看看代码就会发现,operator<<函数能在类外要访问成员变量,就意味着成员变量是用public限定符修饰的,但是一般情况下,成员变量是不让在类外访问的,那么怎么办?

有人或说,那把该函数放在类里面变成成员函数不就行了,但这又会有一个小问题,就是成员函数的第一个参数默认是this指针,那么<<操作符的两个操作数的顺序就会改变,变成x<这里介绍一种新的语法,如下

class Date
{
	friend ostream& operator<<(ostream& out, const Date& d);
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << '/' << d._month << '/' << d._day << endl;
	return out;
}

int main()
{
	Date x(2023, 8, 2);
	Date y(2023, 8, 3);
	cout << x << y;
	return 0;
}

将<<运算符重载函数声明成日期类的友元函数,就可以访问该类的成员对象,可以在类里面的任意位置声明,只要在前面加上friend关键字就行,大家可以自行实现一下输入操作符重载函数

注意事项:

1.友元函数可访问类的私有和保护成员,但不是类的成员函数
2.友元函数不能用const修饰
3.友元函数可以在类定义的任何地方声明,不受类访问限定符限制
4.一个函数可以是多个类的友元函数
5.友元函数的调用与普通函数的调用原理相同

 

2.友元类

和友元函数很相似的一个概念

1.友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
2.友元关系是单向的,即A是B的友元类,那么A可以访问B,但是B不能访问A
3.友元关系不能传递,即A是B的友元类,B是C的友元类不代表A也是C的友元类
4.友元关系不能继承(这点先了解一下,等后面的继承部分会具体讲)

举例如下

class A
{
	friend class B;//注意:这里声明的是B是A的友元类,即B可以访问A中的成员变量
private:
	int _a = 1;
	int _b = 1;
};

class B
{
private:
	int _x;
	A _y;
public:
	void Print()
	{
		cout <<  _y._a << " " << _y._b;//访问私有成员
	}
    void func(const A& d)//友元函数
	{
		cout <<  d._a << " " << d._b;
	}
};

四、内部类

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

注意:内部类就是外部类的友元类

特性:
1. 内部类可以定义在外部类的public、protected、private都是可以的(受到限定符的制约)
2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
3. sizeof(外部类)=外部类,和内部类没有任何关系

class A
{
	class B
	{
	private:
		int _x;
		int _y;
	public:
		B()
			:_x(1)
			, _y(1)
		{}
		void func(A x)//友元函数
		{
			//直接访问外面类的静态成员
			cout << _c;
			Func();
			//
			cout<< x._a;
		}
	};
private:
	int _a;
	static int _c;
	B _b;
public:
	static void Func()
	{
		cout << "hh" << endl;
	}
};

五、匿名对象(创建对象时不给名字)

class A
{
private:
	int _a = 1;
	int _b = 1;
public:
	A()
	{
		cout << "A()" << endl;
	}
	void func()
	{
		cout << "hh" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
};

int main()
{
	A a;
	A();//匿名对象的生命周期就只有这一行
	cout << "------" << endl;
	A().func();//当我们只为了调用类里面的某个函数时,就可以用匿名对象
	return 0;
}

C++入门篇4---类和对象 (下)_第4张图片

 当然这匿名对象的使用场景还是很多的,等到后面用到,会再提,这里先给大家认识一下

六、拷贝对象时的一些编辑器优化(了解即可)

在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还
是非常有用的。注意:这个优化没有标准规定,不同的编辑器的优化会稍有区别,但大部分满足如下几点

1.隐式类型,连续 构造+拷贝构造=>优化为直接构造

2. 一个表达式中,连续 构造+拷贝构造=>优化为一个构造

3.一个表达式中,连续 拷贝构造+拷贝构造=>优化一个拷贝构造

总结一下:在一个表达式中,有连续构造或者拷贝构造,编辑器就有可能进行优化,具体的还得看情况

你可能感兴趣的:(c++,java,算法)