文章目录
1、再谈构造函数
1.1 构造函数的赋值
在之前,我们对设计的构造函数对类中变量给了一个初始值,但这是一个赋值行为,并不是初始化,因为初始化只能进行一次,而赋值可以进行多次。
class Test
{
public:
Test(int e1 = 1, int e2 = 2, int e3 = 3)
{
_e1 = e1;
_e2 = e2;
_e3 = e3;
}
private:
int _e1;
int _e2;
int _e3 = 4;
};
并且,在C++11中的一个新特性,可以在成员变量声明的时候给一个缺省值,这也不能叫做初始化,因为只是声明没有开空间。
1.2 构造函数的初始化
那么为什么要有初始化呢?
我们知道,当一个const变量定义时,必须要初始化。
const int a = 10; //const变量必须初始化
如果类的成员变量中有一个const成员变量时,就不能对它进行赋值了,相应构造函数按照上述写也就报错了。
构造函数初始化写法:
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟
一个放在括号中的初始值或表达式。
class Test
{
public:
Test(int e1 = 1, int e2 = 2, int e3 = 3)
:_e2(e2)
,_e1(e1)
,_e3(e3)
{}
private:
int _e1;
const int _e2;
int _e3 = 4;
};
当然初始化和赋值也可以分开写,只是需要初始化的必须写。
class Test
{
public:
Test(int e1 = 1, int e2 = 2, int e3 = 3)
:_e2(e2)
{
_e1 = e2;
_e3 = e3;
}
private:
int _e1;
const int _e2;
int _e3 = 4;
};
【注意】
-
每个成员变量只能初始化一次。
-
类中出现以下成员,必须放在初始化列表中。
- const 成员变量
- 引用 成员变量 (因为引用必须初始化)
- 自定义类型成员变量(并且没有默认构造函数的)
以下三种必须放在初始化列表
//A中没有默认构造函数
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a = 1, int b = 2)
:_e1(a)
, _e2(b)
, _e3(10)
{}
private:
A _e1; // 没有默认构造函数 (无参、全缺省和编译器默认生成的构造函数)
int& _e2; // 引用
const int _e3; // const
};
能使用初始化列表尽量使用初始化列表,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
这也是下面为什么会先打印A,再打印B的原因。
class A
{
public:
A(int a = 1)
:_a(a)
{
cout << "A(int a = 1)" << endl;
}
private:
int _a;
};
class B
{
public:
B()
{
cout << "B()" << endl;
}
private:
A _e1;
int _e2;
};
int main()
{
B b1;
return 0;
}
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
class B
{
public:
B(int e2 = 1)
:_e2(e2)
,_e1(_e2)
{}
private:
int _e1;
int _e2;
};
int main()
{
B b1;
return 0;
}
所以是先给_e1初始化再给_e2初始化。
1.3 explicit的用法
隐式类型转换
在之前对于const有以下一个场景
int i = 0;
//i的临时变量类型转换成double赋值给d
double d = i;
//rd引用i的临时变量
const double& rd = i;
而在拷贝构造那一节又有这样一个场景
class A
{
public:
A(int a = 1)
: _a1(a)
{}
private:
int _a1;
};
int main()
{
//构造
A d1(2022);
//拷贝构造
//构造一个临时对象储存2022 再拷贝给d2
//这一块vs编译器做了优化使得只需要构造
A d2 = 2022;
//整型构造一个临时变量 d3引用这个变量
const A& d3 = 2022;
return 0;
}
这两个场景有些相似,都是通过临时变量完成了隐式类型转换。
以上程序都正常运行,而通过explicit修饰构造函数,禁止类型转换。
这些都是单参数的情况
class A
{
public:
explicit A(int a = 1)
: _a1(a)
{}
//当然以下写法也是所谓的单参数
//A(int a = 1, int b = 2, int c = 3);
//A(int a)
//A(int a, int b = 2, int c = 3);
private:
int _a1;
};
int main()
{
//构造
A d1(2022);
//A d2 = 2022; err
//const A& d3 = 2022; err
return 0;
}
多参数情况
依然会隐式类型转换,值得注意的是传参
class C
{
public:
//explicit C(int a, int b, int c) 不支持类型转换了
C(int a, int b, int c)
:_c1(a)
,_c2(b)
,_c3(c)
{
}
private:
int _c1;
int _c2;
int _c3;
};
int main()
{
C d1 = { 2022, 10, 13 }; //构造+拷贝构造 但编译器优化只有构造
//等价于
C d2(2022, 10, 13); //构造
//道理和前面一样 类型转换
const C& d3 = { 2022, 10, 13 };
return 0;
}
总结:
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值
的构造函数,还具有类型转换的作用。
上述代码可读性不是很好,用explicit修饰构造函数,将会禁止构造函数的隐式转换。
2、static成员
如果我们要计算一下构造和拷贝构造函数总共调用多少次,设置成普通变量不行,因为它随着变量的销毁也就没了,那如果设置成静态的我们该如何操作它呢?
首先静态变量肯定储存在静态区(数据段),而类储存在栈区,所以静态变量不能在类中定义。
C++规定我们静态变量在类外定义,不过需要指定类域。
不过类中是肯定可以使用静态变量的。
如果想在类外调用私有成员变量,我们只好弄一个成员函数操作私有成员变量。
不过普通的成员函数有this指针,会需要先创建一个变量才能调用。
这就有了静态成员函数,它具有以下特征:
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
声明为静态成员函数使得我们只需要通过类域就能调用静态成员函数
int D::N = 0;
class D
{
public:
D(int d = 0)
:_d1(d)
{
++N;
}
D(const D& d)
:_d1(d._d1)
{
++N;
}
//不受类域限制 只是在类中声明
static void GetN()
{
//这个静态函数不能访问非静态的成员函数了
/*cout << _d1 << endl;*/
cout << N << endl;
}
private:
int _d1;
static int N;
};
//静态成员
int main()
{
D dd1(1);
D dd2 = 3;
D dd3 = dd1;
dd1.GetN();
//因为函数不在类中 不发生解引用 但编译器能通过类找到
D* aa = &dd1;
aa->GetN();
//因为函数不在类中 所以不发生解引用
D* ptr = nullptr;
ptr->GetN();
//静态成员函数 加static 没有this指针 就不用传对象直接用类域
D::GetN();
return 0;
}
总结:
- static成员变量的定义需要在类外,并且存放在静态区。
- 静态成员都可以用 对象.或类:: 调用
- 静态成员函数没有this指针,并且不能访问非静态成员
- 静态成员也是类成员,受访问限定符的限制。
3、友元
友元是一种突破封装的做法,有时提供变量,但是破坏了封装,不宜多用。
友元只分为,友元函数和友元类,没有友元变量
3.1 友元函数
如果一个类外函数想要调用类中的私有成员变量,我们可以将函数作为一个友元函数添加到类中。
如:
class Date
{
friend void Print(const Date& d);
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
void Print(const Date& d)
{
cout << d._year << " " << d._month << " " << d._day << endl;
}
这样Print就可以正常使用,友元函数除了能调用类中私有变量外,都和普通函数一样。
3.1 友元类
友元关系是单向的,不具有交换性。
我在你的类中声明友元,我可以用你的所有私有成员,但你不能用我的私有成员。
class Time
{
// 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
friend class Date;
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;
};
4、匿名对象
匿名对象能方便我们一些场景的使用,下面着重关注
匿名对象的形态和匿名对象的生命周期
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
class Solution
{
public:
int Sum_Solution(int n)
{
//...
return n;
}
};
int main()
{
//这是一个普通的调用
A aa1(1);
//这是一个错误的调用 编译器会识别为函数声明
//A aa2()
//这样就是一个匿名对象
//不过它的生命周期就这一行
A();
//匿名对象好处,调用函数简便
Solution().Sum_Solution(10);
return 0;
}
5、拷贝对象时的一些编译器优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还是非常有用的,同时使得程序效率提高
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a == aa._a;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
void f1(A aa)
{}
A f2()
{
A aa;
return aa;
}
A f3()
{
//A aa(10);
//return aa;
//这种是对上面的优化
return A(10);
}
int main()
{
//隐式类型 构造+拷贝构造 -> 优化为直接构造
//场景1
A aa = 1; //构造和拷贝构造 -> 优化为构造
//场景2
A aa1(1); //构造
f1(aa1); //拷贝构造
//匿名对象调用构造再传值
f1(A(1)); //构造+拷贝构造 -> 构造
//隐式类型构造再传值
f1(1); //构造+拷贝构造 -> 构造
//场景3
f2(); //构造+拷贝构造
//因为函数会返回临时变量 所有最后有个拷贝构造
A ret = f2(); //构造+拷贝构造+拷贝构造 -> 构造+拷贝构造
//这样写就效率很低 不会有任何优化
A ret1;
ret1 = f2();
//场景4
//
A ret2 = f3(); //构造+拷贝构造+拷贝构造 -> 构造
return 0;
}
类与对象完~