一、再谈构造函数
初始化列表
我们先从上一章的栈开始讲
Stack 不具备默认构造,Myqueue 也无法生成默认构造,那我们只能去显示的写构造
- 这个时候就需要用到我们的初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量" 后面跟一个放在括号中的初始值或表达式。
👇代码实现
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
- 【注意】
- 每个成员变量在初始化列表中只能出现一次
- 类中包含以下成员,必须放在初始化列表位置进行初始化:
⭐引用成员变量
⭐const成员变量
⭐自定义类型成员(且该类没有默认构造函数时)
🔺所以对于第一个问题,我们就很好的解决了
//Stack 不具备默认构造, MyQueue 也无法生成默认构造
class MyQueue
{
public:
MyQueue(int n)
:_pushst(n)
,_popst(n)
,_size(0)
{}
private:
//
Stack _pushst;
Stack _popst;
int _size;
};
但是有一个必须在初始化列表初始化:那就是const
-
因为const必须在定义的时候就初始化
-
相同的,引用在定义的时候初始化
⭐ 根据这个,我们可以得出以下结论
- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
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 随机值
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
答案👇
题目
- 一个小小的知识点~
内置类型转换成自定义类型
int main() {
A aa1(1);
A aa2 = aa1; //拷贝构造
A aa3 = 3; //本质是隐式类型转换
/*
* 3构造一个A的临时对象,在用这个临时对象拷贝构造aa3
* 编译器遇到连续构造 + 拷贝构造 -> 优化为直接构造
*/
// raa 引用的是类型转换中用3构造的临时对象
const A& raa = 3;
aa1.Print();
}
- 让我们来看一下下面的代码
class Stack
{
public:
void Push(A aa)
{
//...
}
};
int main()
{
Stack st;
A a1(1);
//这里有什么弊端呢?
//那就是我们还要进行拷贝构造
st.Push(a1);
return 0;
}
自定义类型是需要拷贝的,那么在上面函数里面就浪费了
有报错?没问题,就跟我们上部分讲的一样。
这样就没问题了!
void Push(const A& aa)
explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
class Date
{
public:
// 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用
// explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译
explicit Date(int year)
:_y ear(year)
{}
/*
// 2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具
有类型转换作用
// explicit修饰构造函数,禁止类型转换
explicit Date(int year, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
*/
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1(2022);
// 用一个整形变量给日期类型对象赋值
// 实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值
d1 = 2023;
// 将1屏蔽掉,2放开时则编译失败,因为explicit修饰构造函数,禁止了单参构造函数类型转换的作用
}
啊对,这个就是很简单的,不要忘记了喔
二、Static成员
一个类里面的普通的成员变量,和静态的成员变量,他们两个有什么区别呢?
int _a1;
static int _scount;
概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。
静态成员变量一定要在类外进行初始化
- 推断以下输出的值为多少
class A
{
private:
int _a1 = 1;
int _a2 = 1;
static int _scount;
};
int main()
{
A aa1;
cout << sizeof(aa1) << endl;
return 0;
}
答案是8,两个int分别是4,欸?static修饰的_scont去哪了
🔺其实是,它存在静态区,不存在对象中
- 刚才编译还有报错的地方,我们接着看
- 所以第一个代码为什么会报错捏?
- 因为只有声明没有定义
======== 修改后的代码为:👇 ========
class A
{
private:
//声明
int _a1 = 1;
int _a2 = 1;
//静态区,不在对象中
//不能给缺省值,因为缺省值是给初始化列表的
//虽然他是对象成员,但是并没有存在对象中,所以不走初始化列表
static int _scount;
};
int A::_scount = 1;
int main()
{
A aa1;
cout << sizeof(aa1) << endl;
return 0;
}
★那么结果跑出来了,确实是8!
//每次构造就++ , 析构--
A() { ++_scount; }
A(const A & t) { ++_scount; }
~A() { --_scount; }
//静态成员函数
static int GetACount() { return _scount; }
- 静态修饰函数和变量的意义是不一样的喔,这点我们需要知道
- 变量: 影响生命周期
- 函数: 影响链接属性,没有this指针
意味着只能访问静态成员
特性
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
- 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制
三、 友元
1.友元函数
- 友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在
类的内部声明,声明时需要加friend关键字。
说明
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
2.友元类
- 友元关系是单向的,不具有交换性。
friend class Date; //Date是该Time的友元
声明日期类为Time类的友元类,
则在日期类中就直接访问Time类
Date中可以访问Time的私有,但是Time中不能访问Date的私有
四、 内部类
🔺概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。
class A
{
private:
static int k;
int h;
public:
void func()
{}
class B // B天生就是A的友元
{
public:
void foo(const A& a)
{}
private:
int _b;
};
};
- 所以需要这样写✍,他们受类域的限制
五、匿名对象
匿名对象,生命周期只在当前这一行
用代码更方便的理解一下
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;
// 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
//A aa1();
// 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,
// 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
A();
A aa2(2);
// 匿名对象在这样场景下就很好用,当然还有一些其他使用场景,这个我们以后遇到了再说
Solution().Sum_Solution(10);
return 0;
}
那么在这里,我们的类和对象就正式完结咯~
【撒花】
接下来我还会开内存管理和模板,小小的关注就可以更快的知道更新哟。