目录
在之前的两篇文章中,我们简单的分析了类和对象的一些知识,在这篇文章,主要是对上两篇中一些内容进行补充。
一、再谈构造函数
1.1 构造函数体赋值
构造函数体赋值就是我们在之前的两篇文章中的方式,例如一个日期类的构造函数:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
通过调用上述的构造函数之后,对象中会有一个初始值,但是不能将他称为对象中成员变量的初始化,构造函数体中的语句实际上相当于赋值,初始化只能初始化一次,构造函数体内可以有多次赋值。
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;
};
C++规定是在初始化列表进行初始化操作,初始化只能有一次,所以每个成员变量在初始化列表中最多只能出现一次。
类中包含以下成员的话,必须放在初始化列表位置进行初始化:
- 引用成员变量
- const成员变量
- 自定义类型成员(且该类没有默认构造函数时)
接下来我们根据以下的一段代码进行具体分析:
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int& ref)
: _aobj(a)
, _ref(ref)
, _n(10)
{}
private:
A _aobj; //没有默认构造
int& _ref; //引用
const int _n; //const
};
为什么引用成员变量和const成员变量必须在初始化列表进行初始化?
初始化列表是进行初始化的地方,即对象的成员定义的地方,所以引用成员变量和const成员变量必须在初始化列表进行初始化。
为什么自定义类型成员(且该类没有默认构造函数时)必须在初始化列表进行初始化?
自定义类型必须调用它的构造函数,在定义时必须初始化,如果有默认构造编译器会在初始化列表阶段自动去调用它的默认构造函数,没有默认构造时,在初始化列表阶段由程序员自己调用。
1.3 注意
我们来看下面的一段代码:
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();
return 0;
}
上述代码的打印结果是什么呢?
为什么是上述的结果?
这里引出了一个特性,成员变量在类中声明次序就是其在初始化列表中的初始化顺序,和它在初始化列表中的先后次序无关。
根据这一特性,建议声明的顺序和定义的顺序保持一致。
1.4 总结
初始化列表是进行成员定义的位置,在初始化列表进行初始化操作,所有的成员都会走初始化列表如果我们在初始化列表写了成员的定义编译器就使用我们写的,如果我们在初始化列表没有写某个成员的定义,编译器会自动生成或调用。
class Stack
{
//编译器会生成默认构造函数
private:
int _top;
int* _a;
int _capacity;
};
class MyQueue
{
public:
MyQueue() //在这里我们没有对自定义类型的_pushst和_popst进行初始化,但是所有的成员都会走初始化列表(他是成员定义的位置),编译器会自动去调用它的默认构造
{}
private:
Stack _pushst;
Stack _popst;
};
补充知识:
默认的构造函数有三类:
- 无参的构造函数
- 我们没写编译器默认生成的构造函数
- 全缺省的构造函数
class Stack
{
public:
Stack(int capacity) //不是默认构造函数
{
_a = (int*)malloc(sizeof(int) * capacity);
_top = 0;
_capacity = capacity;
}
private:
int _top;
int* _a;
int _capacity;
};
class MyQueue
{
public:
//如果自定义类型的成员没有默认构造函数,必须显式的去调用
MyQueue(int capacity)
:_pushst(capacity)
,_popst(capacity)
{}
private:
Stack _pushst;
Stack _popst;
};
构造函数体是成员赋值的位置,初始化列表不能代替函数体赋值,总有一些工作是初始化列表无法完成的,例如下例:
class Stack
{
public:
Stack(int capacity)
:_a((int*)malloc(sizeof(int) * capacity))
,_top(0)
,_capacity(capacity)
{
if (nullptr == _a)
{
perror("malloc fail");
exit(-1);
}
}
private:
int _top;
int* _a;
int _capacity;
};
类似于上例,如果我们想要开辟一个数组,当申请空间后,想要判断空间是否申请成功的时候,我们就需要在构造函数体内进行判断。
class AA
{
public:
AA(int row = 10,int col =10)
:_row(row)
,_col(col)
{
_aa = (int**)malloc(sizeof(int*) * row);
for (int i = 0; i < row; i++)
{
_aa[i] = (int*)malloc(sizeof(int) * col);
}
}
private:
int** _aa;
int _row;
int _col;
};
其次在上例中,我们要创建一个二维数组,肯定要用到循环,这时就必须有函数体。
二、拷贝对象时的一些编译器优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝。
有一个A类,
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;
};
下面我们对几种实例化对象的写法进行分析,观察编译器的优化以及在今后的学习生活中,更适合使用哪一种写法。
写法一:
A aa1(1);
上述是实例化一个A类型的对象,并且进行初始化,给_a赋值。
A aa2 = 2;
上述是一个隐式类型转换,整型转换为自定义类型。
const A& aa3 = 2;
上述代码,aa3是2构造出来的A类型的临时对象的别名,临时变量具有常属性,所以这里必须加const。
对于函数调用返回值,也会进行优化:
三、static成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称为静态成员变量,用static修饰的成员函数称为静态成员函数,静态成员变量一定要在类外进行初始化。
3.1 静态成员变量
3.1.1 引入
有这样的一个场景,有一个A类,想要去统计A到底创建了多少个对象,即系统中,当前还有多少个对象正在使用?
我们可以定义一个全局变量_scount,当调用构造(包含拷贝构造)函数的时候,_scount++,当调用析构函数时,_scount--;
int _scount = 0;
class A
{
public:
A()
{
++_scount;
}
A(const A& t)
{
++_scount;
}
~A(){
--_scount;
}
};
A aa0;
A Func(A aa1)
{
cout << __LINE__ << ":" << _scount << endl;
return aa1;
}
int main()
{
cout << __LINE__ << ":" << _scount << endl;
A aa1;
static A aa2;
cout << __LINE__ << ":" << _scount << endl;
Func(aa1);
cout << __LINE__ << ":" << _scount << endl;
return 0;
}
但是上面的代码有一个弊端,_scount是全局变量,全局变量在任何地方都可以随意改变,这就会造成一定的风险。
基于全局变量的弊端,我们能想到如果把他像类的成员变量封装起来,那么就不能在类外面进行随意地改变了。这时就很容易想到了静态成员变量。
3.1.2 特点
class A
{
public:
A()
{
++_scount;
}
A(const A& t)
{
++_scount;
}
~A() {
--_scount;
}
private:
//成员变量
int _a1 = 1;
int _a2 = 2;
//静态成员变量
static int _scount; //声明
};
//全局位置,类外面定义
int A::_scount = 0;
如果静态成员变量是公有的:
我们可以通过下面的方式进行访问:
cout<<A::_scount<<endl;
//d是A类型的对象
count<<d._scount<<endl;
如果静态成员变量是私有的,要通过公有的成员函数来访问,在静态成员函数处讲解。
3.1.3 区别
类里面除了可以定义成员变量,还可以定义静态成员变量。
3.2 静态成员函数
3.2.1 引入
在刚刚定义的A类中,如果静态成员变量_scount是私有的,那么只有通过公有的成员函数才能对他进行访问,在类外面访问成员函数,由于成员函数的第一个指针是隐含的this指针,所以调用成员函数需要有对象,如果我们没有创建对象,怎么办呢?把函数写为静态成员函数。
3.2.2 特点
用static修饰的成员函数称为静态成员函数,静态成员函数没有this指针,指定类域和访问限定符就可以访问。
对于上面的A类,静态成员变量_scount是私有的,可以通过下面的静态成员函数进行访问,完整代码如下:
class A
{
public:
A()
{
++_scount;
}
A(const A& t)
{
++_scount;
}
~A() {
--_scount;
}
static int GetACount()
{
return _scount;
}
private:
//成员变量
int _a1 = 1;
int _a2 = 2;
//静态成员变量
static int _scount; //声明
};
//全局位置,类外面定义
int A::_scount = 0;
int main()
{
cout << __LINE__ << ":"<<A::GetACount() << endl;
}
注意:
- 静态成员函数不可以调用非静态成员函数,静态成员函数没有this指针。
- 非静态成员函数可以调用静态成员函数,静态成员函数指定类域和访问限定符就可以访问。
3.2.3 例题
例一:求1+2+3+...+n_牛客题霸_牛客网 (nowcoder.com)
这个题要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C),在这里我们提供一种思路,要求他们相加的和,我们要有n次循环,但是这里要求不能使用循环,所以我们创建一个有n个对象的数组,即需要调用n次构造函数,在这n次中不断对变量_ret进行累加。
class sum
{
public:
sum()
{
_ret+=_n1;
_n1++;
}
static int GetRet()
{
return _ret;
}
private:
static int _n1;
static int _ret;
};
int sum::_n1 = 1;
int sum::_ret = 0;
class Solution {
public:
int Sum_Solution(int n) {
sum a[n];
return sum::GetRet();
}
};
这里的_ret变量不能定义为局部变量,定义为全局变量有隐患,所以定义为类的静态成员变量,将变量封装起来。
例二:设计一个类,只能在栈或者堆上创建对象。
假设有一个类A,实例化了以下三个对象
题目要求只能在栈或者堆上创建对象,实例化对象的共同点都是要调用构造函数,在这里可以将构造函数私有,然后写两个静态的函数,完成栈上和堆上对象的创建工作。
class A
{
public:
static A GetStackObj()
{
A aa;
return aa;
}
static A* GetHeapObj()
{
return new A;
}
private:
A()
:_n1(1)
,_n2(0)
{}
int _n1;
int _n2;
int _n3;
};
int main()
{
A::GetStackObj(); //在栈上实例化一个对象
A::GetHeapObj(); //在堆上实例化一个对象
return 0;
}
四、友元
友元提供了一种突破封装的方式,友元可以实现在某一个类外面可以访问类的私有和保护成员。但是友元会增加耦合(关联度更加紧密),破坏了封装,所以友元不宜多使用。
4.1 友元函数
在之前的博客中,在对Date类实现的过程中,我们要实现运算符<<和>>的重载,当时我们就使用到了友元函数,对于<<的重载,不能写成成员函数,成员函数的第一个参数是隐含的this指针,但是<<要求第一个参数即左操作数是cout的输出流对象,所以<<运算符重载必须写到类外面,但是在重载函数中又需要访问Date类中的私有成员变量,这时可以使用友元函数。
友元函数可以直接访问类的私有成员,他是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
实现Date类的博客:(8条消息) 类的六个默认成员函数_李有鱼的博客-CSDN博客
//友元函数在类中的声明可以在任意位置
class Date
{
friend istream& operator>>(istream& in,Date& d);
friend ostream& operator<<(ostream& out, const Date& d);
public:
Date(int year=2023, int month=6, int day=8);
void Print() const
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << " " << d._month << " " << d._day << endl;
return out;
}
istream& operator>>(istream& in,Date& d)
{
int year, month, day;
in >> year >> month >> day;
if (month > 0 && month < 13 && day>0 && day <= d.GetMonthDay(year, month))
{
d._year = year;
d._month = month;
d._day = day;
}
else
{
cout << "非法日期" << endl;
assert(false);
}
return in;
}
4.2 友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员
下面有两个类,其中Date类是Time类的友元类:
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 = 1, 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;
};
五、内部类
5.1 定义
如果一个类定义在另一个类的内部,这个定义在内部的类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。
5.2 注意
5.2.1 特性
5.2.2 外部类对象的大小计算
class A
{
private:
static int k;
int h;
public:
class B
{
public:
B()
{}
private:
int a;
int b;
};
};
上述代码中,B是A的内部类,现在要求A类型的大小。
注意:在计算A类型的时候,不需要计算静态变量k以及B类的大小,静态类不在对象中,他在公共区域,B类的大小也不需要计算,在A类中没有B对象,没有用B类创建对象,B定义在A的类域里,但是不占空间,都只是声明,定义出来的对象才占空间。
5.3 例题
在之前我们写了1+2+3+4+5+.....n的求解,在这里是对它的优化。
求1+2+3+...+n_牛客题霸_牛客网 (nowcoder.com)
#include <regex>
class Solution {
public:
class sum
{
public:
sum()
{
_ret+=_n1;
_n1++;
}
};
int Sum_Solution(int n) {
sum a[n];
return _ret;
}
private:
static int _n1;
static int _ret;
};
int Solution::_n1 = 1;
int Solution::_ret = 0;
什么情况用内部类?当我们期望类是藏在另一个类里面的,别人都访问不到。
六、匿名对象
6.1 举例
下面有一个类A
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout<<"~A()" << endl;
}
private:
int _a;
};
下面就是A类型的有名对象:
A aa(1); //有名对象
下面是A类型的匿名对象:
A(2); //匿名对象
6.2 特性
//error 匿名对象具有常性
//A& ra = A(1);
//正确
//const引用延长匿名对象的生命周期,A(1)这个匿名对象的生命周期延长到和ra的生命周期相同,ra是匿名对象的别名,他的生命周期是在当前函数局部域
const A& ra = A(1);
6.3 写法的探讨
有一个类Solution
class Solution {
public:
int Sum_Solution(int n) {
cout << "Sum_Solution" << endl;
return n;
}
};
下面有两种写法,来调用Sum_Solution函数
//方式一
Solution s1;
s1.Sum_Solution(10);
//方式二
Solution().Sum_Solution(20);
对比两种方式,可以发现方式二的代码更简洁易懂,当我们要调用Sum_Solution函数,就可以使用匿名对象去调用函数。