初始化列表
首先,初始化列表是我们的祖师爷本贾尼博士为了解决在某些成员变量在定义时必须初始化的情况。这个初始化列表其实发生在构造函数之前,也就是实例化整个对象时先对所有的成员都进行了初始化
初始化的概念区分
在之前的博客学习中,我们已经学习了【C++】的六大默认成员函数 ,想必大家已经对构造函数已经比较熟悉了,可是大家是否遇到过,在构造函数后面跟了一个冒号,这个问题让我很是困惑
在了解 初始化列表之前,我们首先回顾两个重要的知识:
1. 构造函数是干嘛的?
答: 用于初始化类中的成员变量
2. 什么是初始化?
答: 在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值
接下来再来看一段代码:
class Date
{
public:
//构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
上面这个Date类是我们之前写过的,这里有一个它的有参构造函数,虽然在这个构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化。构造函数体中的语句只能将其称为【赋初值】,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
class Date
{
public:
Date(int year = 2022, int month = 5, int day = 24)
{
_year = year;
_year = 2023; //第二次赋值
_year = 2024; //第三次赋值
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
既然构造函数体的语句只能称作为赋初值,现在,可否有一种方式进行初始化呢?即初始化列表初始化。
总结
- 我们之前写的构造函数其实并不是对成员变量进行初始化而是进行赋初值。
- 如果想要对成员变量进行初始化,需要用到初始化列表
初始化列表的概念理解
以一个冒号 “ :” 开始,接着是一个以 , 分隔的数据成员列表,每个"成员变量"后面跟一个放在 ()的初始值或表达式
例如如下代码:
class Date
{
public:
//构造函数: -->初始化列表初始化
Date(int year = 2024, int month = 8, int day = 2)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
当然,我可以在初始化列表初始化,也可以在大括号内进行赋值:
Date(int year = 2024, int month = 8, int day = 2)
:_year(year)
, _month(month)
{
_day = day;
}
初始化列表的注意事项
初始化列表可以认为就是对象成员变量定义的地方
每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
类中包含以下成员,必须放在初始化列表位置进行初始化
- 引用成员变量
- const成员变量
- 自定义类型成员(该类没有默认构造函数)
先前我们都知道引用的变量和const变量只能在定义时初始化,而普通的变量在定义时不强求初始化,所以我们就不能按照如下的方式操作:
成员变量为const和引用的时候-----正确的代码为:
class Date
{
public:
//析构函数
Date(int year = 12, int month = 10, int day = 1)
:_year(year), _month(month), _day(day)
{}
void Printf()
{
cout << "year为:" << _year << endl;
cout << "month为:" << _year << endl;
cout << "day为:" << _year << endl;
}
private:
//定义时不强求初始化,后面可以再赋值修改
int _year; //声明
//const修饰的变量 和 引用的变量 需要在定义的时候就进行初始化
const int _month;
int& _day;
};
int main()
{
Date d1;
d1.Printf();
return 0;
}
自定义类型成员(该类没有默认构造函数)同样也得在初始化列表进行初始化:
class A
{
public:
A(int x) //不是默认构造函数,因为接受一个参数
:_x(x)
{}
private:
int _x;
};
class Date
{
public:
Date(int a) //在初始化列表对自定义类型 _aa 进行初始化
:_aa(a)
{}
private:
A _aa;
};
注意这里的条件,一定要是没有默认构造函数的自定义类型成员才得在初始化列表进行初始化,而默认构造函数简单来说就是不需要传参的函数
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
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();
}
A、输出1 1 B、程序崩溃 C、编译不通过 D、1 随机值
答案:D
解析:注意成员变量在类中声明次序就是其在初始化列表中的初始化顺序,既然_a2先声明,则必然进入初始化列表要先执行, _a2(_a1) 。意思是说拿_a1去初始化_a2,不过此时的_a1还是随机值,自然_a2即为随机值,随后执行:_a1(a)。拿a初始化_a1,所以输出的值为1和随机值。
explicit关键字
在我们自己平时写 C++ 代码的时候,较少会用到 explicit关键字 。但是在C++相关的标准类库中,看到explicit关键字的频率还是很高的。既然出现的频率这么高,那么我们就来看看explicit关键字的作用到底是干什么的。
什么是explicit关键字
explicit是C++中的一个关键字,它用来修饰只有一个参数的类构造函数,以表明该构造函数是显式的,而非隐式的。当使用explicit修饰构造函数时,它将禁止类对象之间的隐式转换,以及禁止隐式调用拷贝构造函数。
既然解释中提到了 类的构造函数 那么下面我将从构造函数中详细的给大家,讲解explicit其中的含义。
构造函数还具有类型转换的作用
在理解 explicit 关键字 之前,我们必须要了解构造函数的类型转换作用,以便于我们更好的理解 explicit 关键字
单参构造函数与explicit关键字
还是来说说老朋友日期类,我们通过下面这个日期类进行讲解
class Date
{
public:
// 构造函数
Date(int year)
:_year(year) // 初始化列表
{}
private:
int _year;
int _month = 3;
int _day = 31;
};
对于下面的 d1 很清楚一定是调用了有参构造进行初始化,不过对于 d2 来说,也是一种构造方式
int main()
{
// d1 和 d2 都会调用构造函数
Date d1(2022);
Date d2 = 2023;
return 0;
}
我们依旧通过调试来看就会非常清晰,这种 【Date d2 = 2023】 写法也会去调用构造函数
此时,大家可能会产生疑问,这种构造方式从来没有见过,为什么 Date d2 = 2023 会调用 构造函数呢? 其实这都是因为有【隐式类型转换】的存在,下面我将从一个简单的例子来为大家讲解。
像下面将一个int类型的数值赋值给到一个double类型的数据,此时就会产生一个隐式类型转换
int i = 1;
double d = i;
对于类型转换而言,这里并不是将值直接赋值给到左边的对象,而是在中间呢会产生一个临时变量,例如右边的这个 i 会先去构造一个临时变量,这个临时变量的类型是 [double] 。把它里面的值初始化为 1,然后再通过这个临时对象进行拷贝构造给d,这就是编译器会做的一件事
那对于这个 d2 其实也是一样,2023会先去构造一个临时对象,这个临时对象的类型是[Date]
把它里面的year初始化为2023,然后再通过这个临时对象进行拷贝构造给到d2
不是说构造函数有初始化列表吗?拷贝构造怎么去初始化呢?
别忘了【拷贝构造】也是属于构造函数的一种哦,也是会有初始化列表的
//拷贝构造
Date(const Date& d)
:_year(d._year)
,_month(d._month)
,_day(d._day)
{}
刚才说到了中间会产生一个临时对象,而且会调用构造 + 拷贝构造,那此时我们在Date类中写一个拷贝构造函数,调试再去看看会不会去进行调用
- 很明显没有,我在进入Date类后一直在按F11,但是却进不到拷贝构造中,这是为什么呢?
原因其实在于编译器在这里地方做了一个优化,将【构造 + 拷贝构造】优化成了【一个构造】,因为编译器在这里觉得构造再加拷贝构造太费事了,干脆就合二为一了。其实对于这里的优化不同编译器是有区别的,像一下VC++、DevC++可能就不会去优化,越是新的编译器越可能去进行这种优化。
但是怎么知道中间赋值这一块产生了临时对象呢?如果不清楚编译器的优化机制这一块肯定就会认为这里只有一个构造
这点确实是,若是我现在不是直接赋值了,而是去做一个引用,此时会发生什么呢?
Date& d3 = 2024;
可以看到,报出了一个错误,原因就在于d3是一个Date类型,2024则是一个内置类型的数据
一个常量让d3共用会造成权限放大!!
- 但若是我在前面加一个
const
做修饰后,就不会出现问题了,这是为什么呢? - 其实这里的真正原因就在于产生的这个【临时变量】(临时变量具有常性),它就是通过Date类的构造函数构造出来的,同类型之间可以做引用。还有一点就是临时变量具有常性,所以给到一个
const
类型修饰对象不会有问题
从这里我们就可以看到在中间赋值的时候是产生了临时变量。
但若是你不想让这种隐式类型转换发生怎么办呢?此时就可以使用到C++中的一个关键字叫做explicit
- 它加在构造函数的前面进行修饰,有了它就不会发生上面的这一系列事儿了,它会【禁止类型转换】
explicit Date(int year)
:_year(year)
{}
多参构造函数与explicit关键字
//多参构造函数
Date(int year, int month ,int day = 31)
:_year(year)
,_month(month)
,_day(day)
{}
根据从右往左缺省的规则,我们在初始化构造的时候要给到2个参数,d1
没有问题传入了两个参数,但是若是像上面那样沿袭单参构造函数这么去初始化还行得通吗?很明显不行,编译器报出了错误
这个时候就要使用到我们C++11中的新特性了,在对多参构造进行初始化的时候在外面加上一个{}
就可以了,可能你觉得这种写法像是C语言里面结构体的初始化,但实际不是,而是在调用多参构造函数
Date d2 = { 2023, 3 };
- 不仅如此,对于下面这种也同样适用,调用构造去产生一个临时对象
const Date& d3 = { 2024, 4 };
那要如何去防止这样的隐式类型转换的发生呢,还是可以使用到explicit
关键字吗?
//多参构造函数
explicit Date(int year, int month ,int day = 31)
:_year(year)
,_month(month)
,_day(day)
{}
还有一种例外,当缺省参数从右往左给到两个的时候,此时只需要传入一个实参即可,那也就相当于是单参构造explicit
关键字依旧可以起到作用
explicit Date(int year, int month = 3,int day = 31)
:_year(year)
,_month(month)
,_day(day)
{}
友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以
友元不宜多用。
友元分为:友元函数和友元类
友元函数
去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。
但是在类外定义的话没办法直接用类里面的私有成员,如果强行变成公有就破坏了封装性,所以这里会用到友元的知识,友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
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)
{}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
注意:
1、友元函数可访问类的私有和保护成员,但不是类的成员函数
2、友元函数不能用const修饰(没有this指针)
3、友元函数可以在类定义的任何地方声明,不受类访问限定符限制
4、一个函数可以是多个类的友元函数
5、友元函数的调用与普通函数的调用原理相同
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类
中的私有成员变量
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
{
public:
Date(int year = 1900, 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;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
注意:
1、友元关系是单向的,不具有交换性。
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接
访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
2、友元关系不能传递
如果C是B的友元, B是A的友元,则不能说明C时A的友元。
3、友元关系不能继承
内部类
内部类的原理:B的房子的图纸在A的图纸里面。比如说我是地产大亨,你之前帮助过我。最近你跟我说你要在你工作的地方建个别墅。我说好,你建吧,并且我还附赠你一个别墅,把你老家也建造起来。此时 你工作的别墅就不包含你老家的别墅。然后你说,工作的别墅我不要了,能不能单独给我老家建起来,大亨说:你还想空手套白狼是吧 想得美!
内部类的定义
内部类是在一个类的成员部分定义的另一个类。
内部类是⼀个独立的类。跟定义在 全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
计算外部类对象的大小就不会将内部类的成员包括在内
内部类与外部类
内部类访问外部类的成员
简单来说:内部类默认是外部类的友元类
- 内部类可以定义在外部类的public、protected、private中都是可以的。
- 内部类可以直接访问外部类中的static、枚举成员、不需要外部类的对象名。
- 内部类访问外部类的普通成员,需要借助外部类对象(否则无法得知访问的是哪一个对象的)
这里cout<<h<<endl;是一个非常常见的错误。因为内部类是一个独立的类,不属于外部类,所以此时还没有外部类的对象,显然也不存在h。
而k就不同了,静态成员不需要外部类的对象就已存在,所以这里k是OK的。
想要在内部类访问外部类的普通成员,就需要通过外部类对象的方式,比如下方代码,传递一个外部类对象作为参数就可以访问外部类成员
内部类的经典实例
使用的特点就是内部类天生是外部类的友元
class Solution {
public:
int Sum_Solution(int n)
{
Sum a[n];//根据创建数组多少次就调用多少次构造函数
return _sum;
}
private:
class Sum
{
public:
Sum()
{
_sum+=_i;
++_i;
}
};//内部类可以访问外部类的静态成员
static int _i;
static int _sum;
};
int Solution::_i=1;
int Solution::_sum=0;
外部类访问内部类的成员
内部类不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。
内部类在定义它的外围类的作用域内是可见的,但在外围类外部是不可见的。
- 假如内部类受外部类公有限定符限制,需要通过外部类限定符限定的方式创建内部类对象
- 假如内部类是受外部私有或保护限定符限制,那么在类外无法创建内部类的对象(如果你不想外部可以创建内部类的对象,就可以这么做)
- 外部类无法访问内部类的私有成员
class A
{
private:
static int _k;
int _h = 1;
public:
class B // B默认就是A的友元
{
public:
void foo(const A& a)
{
cout << _k << endl; //OK
cout << a._h << endl; //OK
}
private:
static int a;
};
void fun()
{
//cout << B::a << endl;
//外部类无法访问内部类的私有成员
}
};
int A::_k = 1;
int main()
{
A::B b;//假如内部类受外部类公有限定符限制,可以这样创建内部类对象
A aa;
return 0;
}
内部类的使用场景
- 封装和隐藏实现细节:内部类可以隐藏实现细节,使得外围类的接口更加简洁。—内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考 虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了
- 实现辅助类:内部类可以作为外围类的辅助类,用于实现一些具体的功能,而不影响外围类的整体结构。
- 避免命名冲突:通过内部类,可以避免不同命名空间或类中的命名冲突。
- 访问权限控制:内部类可以更好地控制对特定成员或方法的访问权限。
匿名对象
匿名对象的生命周期在当前行
匿名对象具有常性
const引用会延长匿名对象的的生命周期,声明周期跟当前函数的作用域
即用即销毁