类和对象(下)[初始化列表、explicit关键字、static成员、C++11缺省值初始化、友元、内部类]

在这里插入图片描述

构造函数(初始化列表和explicit关键字)

函数成员初始化

我们在上一篇种介绍构造函数的时候,书写了一种初始化类的对象的一种方法,就是写构造函数在函数体内进行初始化。现在我们要学习的就是初始化列表

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

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

上面这种就是函数体内初始化。
但是其实上面这个并不能称之为初始化,因为初始化只能进行一次,就是在创建这个对象的时候,上面准确的说是在给创建好的对象赋值,因为赋值可以多次赋值,在这个函数体内就可以多次赋值。

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

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

那这种就是我们的初始化列表,直接在函数下面写,并不在函数的大括号里面,用一个冒号开头中间的用逗号隔开。对象后面的括号里面放的是要初始化的值。也可以放常数。

首先这两种初始化方式是可以混用的,比如像下面这样子

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

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

对象的定义实际上是在初始化列表中定义的,就算没写初始化列表,只是没有给对象初始化,相当于是对象里面放的随机值。

初始化列表的特性

1,每个变量在初始化只能初始化一次,因此类中的每个变量只能在初始化列表中出现一次。
2,以下成员必须放在初始化列表中进行初始化:
引用类型的成员变量,const类型的成员变量,自定义类型成员(没有默认构造函数)
3,尽量使用初始化列表进行初始化,因为无论你用不用初始化,实际调用构造函数的时候都是先使用初始化列表进行初始化的。
4,成员变量初始化的顺序是按照变量声明的顺序的,与你在初始化列表中写的顺序无关。


引用类型成员变量和const类型的成员变量的特点就是他们都必须在定义的时候初始化,定义完了之后他们的值就不可以更改了。如果我们把他们的定义放到构造函数体内定义会报错实际就像下面这样:

int main()
{
    const int i;
    i = 10;
    int e = 20;
    int& re;
    re = e;
}

定义完了之后在赋值,这对于普通变量来说没区别,但是这些是引用和const变量,就出问题了。所以这里就必须放到初始化列表里面初始化。也说明了初始化列表是成员变量定义的阶段,构造函数体内就是赋值的阶段。

class da
{
public:
	da(int x)
	{
		_x = x;
	}

private:
	int _x;
};

class Test
{
public:
	Test(int i = 10, int ra = 20)
		:_i(i)
		,_ra(ra)
		,_x(4)
	{}
private:
	const int _i;
	int& _ra;
	da _x;
};

int main()
{
	Test d1;
	return 0;
}

这里还要注意就是这个ra是局部变量,_ra是ra的别名,ra出作用域就会销毁,所以有越界风险,这里只是演示一下,const和引用类型的变量如何初始化。


那这里还需要复习一下什么是默认构造函数:默认构造函数就是不传参也可以调用的构造函数,有三种:1,不写,编译器默认生成的就是默认构造函数。2,没有参数的构造函数。3,全缺省的构造函数。
也即是说,这个没有默认构造函数的成员变量,创建的时候你不给他传参理论上因为构造函数时完成对象的初始化,系统调用不了构造函数那这个对象里面就是随机值,但是这里因为构造函数是系统自动调用的,如果不传参调用不了默认构造函数就会报错。
这里还有一个点就是:初始化列表其实是成员变量的定义阶段,在这个阶段给成员变量赋值叫做初始化,在构造函数体内给成员变量赋值就是赋值,不是初始化,而且就算我们不写初始化列表,初始化列表也是存在的,只是初始化的时候成员变量里面放的是随机值
但是对于自定义类型,定义的时候会取调用他的构造函数,如果没有默认构造函数,就必须要求我们在初始化的时候给传参,也就是在初始化列表里面给参数,不然系统没有传参调用不了那个构造函数就会报错。

class da
{
public:
	da(int x)
	{
		_x = x;
	}

private:
	int _x;
};

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

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

int main()
{
	Date d1;
	return 0;
}

比如这段代码,da就是一个没有默认构造函数的类,Date类的成员变量包含了da的对象,所以我们在初始化列表必须写出来传参数,然后调用da的构造函数对_x进行初始化,如果放到函数体内就会报错。

下面来看这道题:

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 随机值
回顾我们上面总结的特性第四条,成员变量的初始化顺序是按照声明顺序,与初始化列表中的顺序无关,所以我们先用_a1初始化_a2,然后用1初始化了_a1,因此_a2是随机值。

explicit关键字

先来看一段代码

class Date
{
public:
	Date(int time = 0)
		:_time(time)
	{}
private:
	int _time;
};
int main()
{
	Date d1;
	Date d2 = 20;
	Date d3(39);
}

这里可以看到,调用构造函数的时候,可以有三种方式这里,不传参,第二是传参,第三种就是很特别了,就类似于内置类型的定义的时候赋初值一样。
构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用

这里的d2的初始化是将,20先构造成立一个Date类的临时对象,然后用将这个临时对象拷贝构造了一个d2。但是在现在编译器的优化之下,一般不会生成一个临时对象,而是会直接调用构造函数然后将20作为参数传过去调用构造函数对d2进行初始化。
如果用explicit对这个构造函数进行修饰,那就会禁用这个int类型转换成Date的类型。d2这个就不能这样子写了。

至于这个类型转换的作用,来看下面这段代码,了解一下即可,这些是涉及到stl的知识,后面我也会讲的。

#include<iostream>
#include<string>
#include<vector>
using namespace std;

int main()
{
	vector<string> v1;
	string s1("hello");// s1.string(const char*str);
	v1.push_back(s1);//这个函数原型为:v1.push_back(const string& s);
	v1.push_back("kisskernel");
	return 0;
}

上面我们可以看到,插入了”hello“的时候需要先创建一个s1的对象,然后讲s1传过去插入到顺序表中,而第二个就是用到了类型转换,直接用字符串构造出一个string的临时对象,然后插入到顺序表。可以简化代码。

static成员

概念

static修饰的成员函数和成员变量统称为static成员。
static类型的成员变量必须在类外初始化,并且声明的时候不能带上static。

class Date
{
public:

private:
	static int _n;
	int _time;
};
int _n = 0;
int main()
{

	return 0;
}

就像上面这样初始化_n的时候直接在类外初始化即可。

特性:

1,静态成员存放在静态区,并不在栈区,所以静态成员变量不在对象内,但是每个对象都可以访问到静态成员变量。
2,静态成员变量在类外定义,类里面的只是声明。在类外定义的时候不加static
3,静态成员函数不能访问非静态成员,因为静态成员函数没有this指针。
4,任何成员函数都可以调用静态成员函数
5,类的静态成员可以用类名::静态成员名或者对象.静态成员名,前提是public对象。
6,静态成员和普通成员一样,也有public、protected、private三种访问级别,也可以有具体的返回值
【问题】

  1. 静态成员函数可以调用非静态成员函数吗?
  2. 非静态成员函数可以调用类的静态成员函数吗?

答案:静态成员函数不可以调用非静态成员函数,因为静态成员函数没有this指针。
2,非静态成员函数可以调用类的静态成员函数。

C++11的成员初始化

在c++11中,非静态成员变量可以在声明的时候给他赋值上缺省值,这样如果没有给这些变量初始化,那这些变量就是缺省值。

这里要明白对象的初始化是在构造函数的初始化列表,如果我们写了初始化列表规定了这个变量用什么初始化,那就会优先使用初始化列表,如果没写初始化列表,系统就会默认使用这个声明的缺省值来初始化变量。
在这里插入图片描述

当然如果是自定义类型的也可以赋值上缺省值,前提是这个类的构造函数得是全缺省,或者只有第一个参数没有缺省值,不然会存在调不动自定义类型的构造函数这个错误。

class B
{
public:
	B(int ar = 0)
		:_ar(ar)
	{}
	void printb()
	{
		cout << _ar << endl ;
	}
private:
	int _ar;
};

class A
{
public:

	void print()
	{
		cout << _x << endl << _y << endl;
		_b.printb();
	}
private:
	int _x = 10;
	int _y = 10;
	B _b = 20;
};

int main()
{
	A a;
	a.print();
	return 0;
}

如果是下面这种情况

class B
{
public:
	B(int ar ,int br ,int cr = 10)
		:_ar(ar)
		,_br(br)
		,_cr(cr)
	{}
	void printb()
	{
		cout << _ar << endl ;
	}
private:
	int _ar;
	int _br;
	int _cr;
};

class A
{
public:

	void print()
	{
		cout << _x << endl << _y << endl;
		_b.printb();
	}
private:
	int _x = 10;
	int _y = 10;
	B _b = 20;
};

int main()
{
	A a;
	a.print();
	return 0;
}

B的构造函数有两个没有缺省值,这时候给_b赋值20,不能发生类型转换,所以_b的初始化就会因为缺少参数调用不了B的构造函数,所以就会报错。
缺省值不一定必须是常量也可以时malloc空间,比如int*的变量

友元

1,友元函数

友元是可以帮助我们突破封装,在类外访问到类内的私有成员,但是友元会增加程序的耦合性,不建议过多使用。
下面来看友元的使用场景,重载<<和>>。

class Date
{
public:
	Date(int year = 0, int month = 1,int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
	
	ostream& operator<<(ostream& _cout)
	{
		_cout << _year << _month << _day << endl;
		return _cout;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2022, 7, 3);
	cout << d1;
	return 0;
}

在这段代码里面我们重载了<<符,理论上就可以用cout<<直接输出对象了,可是当我们调用的时候却发现报错了,这是因为什么呢?首先看我们的调用方式,cout<<d1,这个转换一下实际是,cout.operator<<(&cout,d1)
仔细观察,这是不是和我们的参数反过来了呢?在<<左边一定是我们的左参数,右边一定是右参数,但是在类内的成员函数第一个隐含的参数一定时this指针。所以我们不妨这样子调用试试

int main()
{
	Date d1(2022, 7, 3);
	d1 << cout;//d1.operator<<(&d1,cout)
	return 0;
}

这样子,按照参数原型的顺序,就能成功调用了,到那时这样看着会有一些诡异,像是把控制台输出到d1了。所以这时候我们要想调换参数顺序就必须在类外定义这个重载函数了。

class Date
{
public:
	Date(int year = 0, int month = 1,int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& _cout,Date &d)
{
	_cout << d._year << d._month << d._day << endl;
	return _cout;
}
int main()
{
	Date d1(2022, 7, 3);
	cout << d1;
	return 0;
}

像这样,但是这时候就会发现我们在类外访问不到类内的私有成员,现在有三种方法突破类域,第一种方法就是将私有成员变成共有,但是这样做副作用太大。第二种方法,就是我们定义共有函数去取出私有成员的值然后返回,这样很麻烦。第三种方法就是我们的友元函数啦。操作如下:

class Date
{
	friend ostream& operator<<(ostream& _cout, Date& d);
public:
	Date(int year = 0, int month = 1,int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& _cout,Date &d)
{
	_cout << d._year << d._month << d._day << endl;
	return _cout;
}
int main()
{
	Date d1(2022, 7, 3);
	cout << d1;
	return 0;
}

就是上面这样在类体内用friend声明这个函数,就可以让这个类外的函数访问到类内部的私有成员了,相当于给这个函数开了一个后门。

这里注意友元函数的声明在类内的那个位置都可以不一定要在最开头。

下面是输入和输出符号的重载代码:

class Date
{
	friend ostream& operator<<(ostream& _cout, Date& d);
	friend istream& operator>>(istream& _cin, Date& d);
public:
	Date(int year = 0, int month = 1,int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& _cout,Date &d)
{
	_cout << d._year << d._month << d._day << endl;
	return _cout;
}
istream& operator>>(istream& _cin,Date& d)
{
	cin >> d._year >> d._month >> d._day;
	return _cin;
}
int main()
{
	Date d1;
	cin >> d1;
	cout << d1;
	return 0;
}

2,友元类

友元类的意思就是,这个类中的所有成员函数都可以是另一个类中的友元函数,可以访问到零一个类的私有成员。
特性:友元类是单向的,例如Date是Time的友元类,在Date中可以访问到Time的私有成员,但是在Time类中不能访问Date类中的私有成员。
友元类是不具备传递性的,如果B是A的友元,C是B的友元,则不能说明C时A的友元。

class A
{
	friend class Time;
public:
	A(int val = 10)
		:_val(val)
	{}
	
private:
	int _val;
};

class Time
{
	friend class Date;
public:
	Time(int hour = 0)
		:_hour(hour)
	{}
	void PrintTime()
	{
		cout << _aa._val << endl;
	}
private:
	int _hour;
	A _aa;
};

class Date
{

public:
	Date(int year = 0, int month = 1,int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
	void Print()
	{
		cout << _t._hour << endl;
		_t.PrintTime();
		_t._hour = 20;
		_t._aa = 20;
		_t.PrintTime();

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

int main()
{
	Date d1;
	d1.Print();
	return 0;
}

这段代码:Date是TIme的友元,Time是A的友元,所以在Date中可以访问到Time的非公有成员,但是访问不到A的非公有成员,在Time中能访问到A中的非公有成员。

我的理解是:友元类是单向性,一步性,单向性的意思是:Date是Time的友元,但是Time不是Date的友元,Date可以访问Time的私有反之则不行。一步性则是,在那个类中声明了友元,那这个被声明的类只能一步访问到这个声明它是友元的类中,不可以通过访问这个类再去访问到别的类。(例子解释:Time中声明了Date为友元,A中声明了Time 是友元,但是我在Date中只能一步访问到Time的私有成员,不可以通过Time去直接访问A中的成员。)
但是呢,可以通过Time中的函数去间接访问到A的成员。比如上面代码中的PrintTIme函数。

内部类

早在第一节的时候就说过类的定义是可以嵌套的。那内部类的意思就是在类中再次定义一个类,这个内部类相当于是外部这个类的友元类,在内部类中可以通过外部类的对象访问到外部类的所有成员,但是外部类访问不到内部类中的非公有成员,因为外部类就相当于是内部类的类外了。

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

  1. 内部类可以定义在外部类的public、protected、private都是可以的。
  2. 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
  3. 如果是非静态成员必须通过特定的对象来访问。比如d1._year = 20;如此。
  4. sizeof(外部类)=外部类,和内部类没有任何关系

内部类和外部类在空间上并没有关系,所以计算外部类的大小的时候不能算上内部类的大小。他们只是在作用域上外部类包含着内部类,内部类可以访问到外部类的私用成员变量。

class Date
{

public:
	Date(int year = 0, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}
	void Print()
	{
		cout << _t << endl;
	}
	class In
	{
	public:
		void test(const Date&d)
		{
			st = _t;
			_t = d._year;
		}
		static int testnum;
	private:
		int st;
	};
private:
	int _year;
	int _month;
	int _day = 99;
	static int _t;
};
int Date::_t = 0;
int Date::In::testnum = 0;
int main()
{
	Date d1;
	Date::In t1;
	return 0;
}

上面的代码体现了,在内部类中访问static变量不需要加Date::这个作用域限定符,当然如果在外部想要访问到static变量首先要突破类域其次是private限定符。

class A {
private:
	static int k;
	int h;
public:
	void Print()
	{
		cout <<"k: " << k<<"  " << "h: " << h << endl;
	}
	class B
	{
	public:
		void foo(const A& a)
		{
			cout << k << endl;//直接访问静态成员
			cout << a.h << endl;//通过对象访问非静态变量
		}
	};
};
int A::k = 1;
int main()
{
	A::B b;
	b.foo(A());
	A a1;
	a1.Print();
	return 0;
}

通过上面这段代码可以看到,在内部类的成员函数中,要访问外部类的静态成员不需要加类名::或者对象名.
访问非静态变量就需要对象.了

但是这里有一个点就是:匿名对象里面的h值打印出来是0,a1对象里面的值打印出来是随机值,这是为什么呢?
我这里测试使用的是vs2019版本,这个是和编译器有关的,有的编译器会对匿名对象里面的内置类型进行初始化,有的不会进行,所以我们就要当成不初始化。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

KissKernel

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值