1. 初始化列表
1.1 初始化列表的使用
在构造函数中,对成员变量进行初始化可以说是公式化的步骤,而初始化列表就将这一步骤进行了标准化。
初始化列表紧跟在构造函数的参数列表后面,使用方式是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式(每个成员变量只能出现一次)。
初始化列表在对成员变量进行初始化时,依照的是声明的顺序,而与初始化列表中给出的顺序无关,一般情况下,建议将二者的顺序统一。
class 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;
};
括号中只要是有值的表达式皆可,这意味着我们也可以这样写:
:_hour((int)malloc(sizeof(int)))
1.2 必须使用初始化列表进行初始化的变量
之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,例如:
Time(int hour = 0, int minute = 0, int second = 0)
{
_hour = hour;
_minute = minute;
_second = second;
}
这样会存在一个问题,即在函数体中无法对以下两种成员变量进行赋值:
1. const成员变量
2. 引用类型的成员变量
而这两种类型的变量却恰恰必须要进行初始化,否则会报错。
初始化列表的出现,使得这一问题得以解决,在初始化列表中,可以对二者进行初始化。
或者说,必须依赖初始化列表对二者进行初始化。
所以,一般来说,我们认为初始化列表是成员变量被定义的地方,这也就解释了为什么初始化列表中每个成员变量至多只能出现一次。
class test
{
public:
test(const int x, int& y)
:_x(x)
,_y(y)
{}
// 初始化列表完成成员变量的定义
// 效果等同于
// const int _x = x;
// int& _y = y;
private:
// 此处只是声明
const int _x;
int& _y;
}
但是从底层逻辑来讲,这个说法并不完全合理。
因为对象在被定义的时候,成员变量空间的开辟就已经全部完成,而不是在调用构造函数时才开辟的,下面举一个例子来说明这一点:
#include<iostream>
using namespace std;
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2 = 2;
int _a1 = 2;
};
int main()
{
A aa(1);
aa.Print();
}
在这个程序中,因为_a2先被声明,所以在初始化列表中_a2会先进行初始化。
而_a2是通过_a1进行初始化的,假如此时_a1并没有被定义,那么程序一定会报错,但事实上在输出的结果中我们会看到,_a2的值是一个随机值。
这就说明,初始化列表本质上依然只有初始化的功能,但其确实有权限对只能在定义时初始化的变量进行初始化,因此可以认为初始化列表是成员变量定义的地方。
除了上面提到的两种类型的变量之外,没有默认构造函数的自定义类型的变量在作为成员变量时也必须要放到初始化列表,并给出参数,否则会发生报错“没有合适的默认构造函数可用”。
1.3 其他细节
所有的成员变量都会走初始化列表,例如:
class 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;
Type x;
};
其中,“Type”为内置类型或自定义类型。
x虽然没有在初始化列表进行初始化,但其也会走初始化列表。
假如Type为内置类型,则其初始化方式未知;假如Type为自定义类型,则会调用该类型的构造函数来对其进行初始化。
C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显式在初始化列表初始化的 成员使用的。例如:
class Time
{
public:
Time(int hour = 0)
:_hour(hour)
{}
private:
int _hour = 0;
int _minute = 0;
int _second = 0;
};
上面这个类在实例化时,_minute和_second并没有显式在初始化列表初始化,但是在经过初始化列表时,由于在声明处存在缺省值,所以二者也会用缺省值以初始化列表的方式进行初始化。
_hour显式在初始化列表进行了初始化,则其缺省值弃用,按照显示方式初始化。
也就是说,成员变量都会经过初始化列表(static成员除外,接下来会讲到),发生的行为如下图所示:
1.4 总结
1. 每个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方,但并不完全正确。
2. 引用成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始化,否则会编译报错。
3. 尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会走初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没有显式在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有显式在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造会编译错误。
4. 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持⼀致。
2. 类型转换
C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
2.1 实例+机理
class Date
{
public:
Date(int year = 2004, int month = 12, int day = 18)
:_year(year)
,_month(month)
,_day(day)
{}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
d = 2024;
d.Print();
return 0;
}
而上面这段代码中,我们将int类型的2024直接赋值给了Date类类型的d。
这里就发生了隐式的类型转换,具体的机理为:
1. 创建一个Date类类型的临时对象;
2. 将2024作为参数传入对应的构造函数中,对该临时变量进行初始化;
3. 将该临时对象赋值给d;
4. 销毁临时变量。
这样的过程类似于将int类型的变量赋值给double类型的变量:
1. 创建一个double类型的临时变量;
2. 将2024强制类型转换为duoble类型并存到临时变量中;
3. 将该临时变量赋值给double类型的变量;
4. 销毁临时变量。
但是,在套机理中,临时对象的存在似乎并不是很必要。
编译器一般会将这个过程优化为直接构造,即,将等号右边作为参数直接对等号左边的对象进行构造。
C++11以后可支持多参数转化,例如:
Date d1 = { 2004, 12, 18 };
2.2 意义
例如:
Date arr[3] = {{2004, 12, 18}, {2024, 8, 20}, {2024, 9, 1}};
如果不存在隐式类型转换,我们需要将上面的代码写成:
Date d1(2004, 12, 18);
Date d2(2024, 8, 20);
Date d3(2024, 9, 1);
Date arr[3] = {d1, d2, d3};
总结来说,类的类型转换就是方便我们利用临时对象写代码。
3. static成员
被static修饰的成员就叫static成员,也叫静态成员,分为静态成员变量和静态成员函数。
静态成员也是类的成员,受public、protected、private访问限定符的限制。
突破类域就可以访问静态成员,可以通过类名::静态成员或者对象.静态成员来访问静态成员变量和静态成员函数。
3.1 static成员变量
用static修饰的成员变量,称之为静态成员变量,静态成员变量一定要在类外进行定义初始化。
class A
{
private:
// 类里面声明
static int _scount;
};
// 类外面定义初始化
int A::_scount = 0;
与一般的成员变量不同,静态成员变量为所有类对象所共享(有且仅有一个),不属于某个具体的对象,不存在对象中,存放在静态区。
静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员 变量不属于某个对象,不走构造函数初始化列表。
准确来说,static成员变量就是受到类域限制的全局变量。
3.2 static成员函数
用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。
class A
{
public:
// 无this指针
static int GetACount()
{
return _scount;
}
private:
// 类里面声明
static int _scount;
};
// 类外面初始化
int A::_scount = 0;
int main()
{
cout << A::GetACount() << endl;
cout << a1.GetACount() << endl;
return 0;
}
由于没有this指针,所以静态成员函数只能访问静态成员变量,而不能访问非静态的。
当然,非静态的成员函数因为有this指针,所以可以访问任意的静态成员变量和静态成员函数。
3.3 运用练习
//求1+2+3+...+n,
//要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
// 利用按位与
class Solution {
public:
int Sum_Solution(int n) {
n && (n += Sum_Solution(n - 1));
return n;
}
};
// 利用static成员
class Sum
{
public:
Sum()
{
_ret += _i;
_i++;
}
static int GetRet()
{
return _ret;
}
private:
static int _i;
static int _ret;
};
int Sum::_i = 1;
int Sum::_ret = 0;
class Solution {
public:
int Sum_Solution(int n) {
Sum arry[n];
return Sum::GetRet();
}
};
4. 友元
大多数情况下,为了类的封装性,我们会将成员变量设置为私有。
但是,在某些情况下,我们的某个非类的成员的函数又不得不对类的成员变量进行访问。
例如日期类的实现中,对“<<”和“>>”的重载函数C++笔记---日期类实现-CSDN博客。
友元提供了一种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类 声明的前面加friend,并且把友元声明放到一个类的里面。
4.1 友元函数
有了友元声明,外部友元函数就可访问类的私有和保护成员,但友元函数仅仅是一种声明,他不是类的成员函数。
一个函数可以是多个类的友元函数。
// 前置声明,否则A的友元函数声明编译器不认识B
class B;
class A
{
// 友元声明
friend void func(const A& aa, const B& bb);
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
// 友元声明
friend void func(const A& aa, const B& bb);
private:
int _b1 = 3;
int _b2 = 4;
};
void func(const A& aa, const B& bb)
{
cout << aa._a1 << endl;
cout << bb._b1 << endl;
}
int main()
{
A aa;
B bb;
func(aa, bb);
return 0;
}
4.2 友元类
友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员。
class A
{
// 友元声明
friend class B;
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
void func1(const A& aa)
{
cout << aa._a1 << endl;
cout << _b1 << endl;
}
void func2(const A& aa)
{
cout << aa._a2 << endl;
cout << _b2 << endl;
}
private:
int _b1 = 3;
int _b2 = 4;
};
int main()
{
A aa;
B bb;
bb.func1(aa);
bb.func1(aa);
return 0;
}
友元声明就好比是两个人A和B。
A提前向自家的保安(编译器)说明“B是我的好朋友,如果他要访问我家,请放行”(友元声明)。
所以,当B来访问A时可以畅通无阻(B的函数可以访问A的成员变量)。
但在上面的程序中,B并没有对自家的保安进行过说明(没有为A进行友元声明),所以当A来访问B时,保安不会放行(A的函数无法访问到B的成员变量)。
也就是说,友元类的声明并不是让两个类成为了好友可以互相访问,而是对对方放行的一种许可。
1. 友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元。
2. 友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元。
友元有时能提供便利,但是会增加类的耦合度,破坏了封装,所以友元不宜多用。
5. 内部类
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。
内部类是一个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
内部类默认是外部类的友元类。
内部类本质也是一种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考 虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其 他地方都用不了。
例如刚才利用Sum类解决问题时,可以将Sum类设计为Solution类的内部类:
// 设计成内部类
class Solution {
private:
static int _i;
static int _ret;
class Sum
{
public:
Sum()
{
_ret += _i;
_i++;
}
static int GetRet()
{
return _ret;
}
};
public:
int Sum_Solution(int n) {
Sum arry[n];
return Sum::GetRet();
}
};
int Solution::_i = 1;
int Solution::_ret = 0;
6. 匿名对象
用类型(实参)定义出来的对象叫做匿名对象,相比之前我们定义的类型对象名(实参)定义出来的叫有名对象。
匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象。
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
// 定义匿名对象
A();
A(10);
return 0;
}
在调用Solution类中的函数来解决问题时,匿名对象能带来极大的便利:
class Solution
{
public:
int Sum_Solution(int n)
{
//...
return n;
}
};
int main()
{
cout << Solution().Sum_Solution(10) << endl;
return 0;
}