⭐博客主页:️CS semi主页
⭐欢迎关注:点赞收藏+留言
⭐系列专栏:C++初阶
⭐代码仓库:C++初阶
家人们更新不易,你们的点赞和关注对我而言十分重要,友友们麻烦多多点赞+关注,你们的支持是我创作最大的动力,欢迎友友们私信提问,家人们不要忘记点赞收藏+关注哦!!!
我们进入最后一个章节,类和对象的下篇,这里我们会分六个板块进行讲解,每一个板块都是深入浅出,直接出发!
如下是我们经常写的构造函数:
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
1、有些情况下靠着构造函数赋值是不能够满足条件的,所以就创出来了初始化列表来弥补构造函数赋值不足的情况,因为每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。
2、有些成员必须在初始化列表中初始化,这个后续再谈。
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
1、引用成员变量
2、const成员变量
3、自定义类型成员(且该类没有默认构造函数时)
我们先来细细聊一聊2和3这两个特征,const成员变量和自定义类型成员的特征是在定义的时候初始化,我们知道,在函数体内叫赋值,而在定义时候初始化就需要用到初始化列表。
我们再来细细聊一下我们的1:
首先我们需要先明确的一个概念就是什么是默认构造函数,很简单,默认构造函数就是无参的/带参并有初始化赋值的。
尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
我们讲尽量使用初始化列表,那么我们就举初始化列表不能进行方便的例子。
//动态开辟一个二维数组
class A
{
public:
A(int row = 10, int col = 5)
:_row(row)
,_col(col)
{
_a = (int**)malloc(sizeof(int*) * row);
if (_a == nullptr)
{
perror("malloc fail\n");
exit(-1);
}
for (int i = 0; i < row; i++)
{
_a[i] = (int*)malloc(sizeof(int) * col);
}
}
private:
int** _a;
int _row;
int _col;
};
这个例子我们的初始化列表是无法写动态开辟的,因为太长了,所以我们要灵活运用这些知识,尽可能使用初始化列表。
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
是按照类中声明的次序的,所以先赋值的是第二个,而第二个的_a1并没有进行赋值和初始化,所以为随机值,我们称之为“野引用”。
我们可以看一下是否调用了拷贝构造:
所以编译器会进行优化,将原本需要构造和拷贝进行合一成仅需构造,因为会有个临时变量,先构造出再进行拷贝,现在编译器直接合二为一了,直接一步构造函数即可。
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
我们使用explicit关键字可以将原本需要进入构造函数的不让它进入,explicit修饰构造函数,禁止类型转换。
同样explicit关键字同样可以禁止构造函数的隐式类型转换。
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。
int _scount = 0;
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
//static int GetACount() { return _scount; }
private:
//static int _scount;
};
void Func()
{
static A a2;
cout << __LINE__ << ":" << _scount << endl;
}
A a;
int main()
{
A a1;
cout << __LINE__ << ":" << _scount << endl;
Func();
cout << __LINE__ << ":" << _scount << endl;
return 0;
}
大家算一算最终输出的是什么?
答案揭晓:2 3 3。
其实很简单,先定义了一个全局的对象和一个main函数局部的对象,这样子是先调用两次构造函数,而Func函数中的静态变量始终占一个构造函数,因为出了Func函数作用域其不会消失,存放在静态区。所以是2 3 3。
可是我们在main函数中将_scount进行++发现最终输出的结果会发生改变,那么全局变量的劣势就显现出来了,它可以在任意地方进行更改,那么对于整个函数的影响是很大的。
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
// 静态成员函数,没有this指针,指定类域和访问限定符就可以访问
static int GetCount() { return _scount; }
private:
// 成员变量 -- 属于每一个类对象,存储对象里面
int _a = 1;
int _b = 2;
// 静态成员变量 -- 属于类,属于类的每个对象共享,存储在静态区
static int _scount;
};
//类外面定义
int A::_scount = 0;
对于1来讲是不可以的,因为静态成员函数没有this指针,非静态的并不能调过去。
对于2来讲是可以的,因为用不到this指针。
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类。
我们在之前讲到的重载operator<<和operator>>两个函数用到的是友元函数,是将函数体的定义放到全局函数中,没有this指针则两个形参可以随意颠倒,然后友元突破私有的限制在全局的函数体中进行使用。
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加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;
}
说明:
友元函数可访问类的私有和保护成员,但不是类的成员函数
友元函数不能用const修饰
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数
友元函数的调用与普通函数的调用原理相同
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
1、友元关系是单向的,不具有交换性。
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
2、友元关系不能传递
如果B是A的友元,C是B的友元,则不能说明C时A的友元。
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的天然友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
大家算一算sizeof(A)为多少?其实是4。跟静态的和另一个B类都无关,我们可以这样理解,A是外部类,是一个大图纸,B是内部类,是A的图纸中夹了一层图纸,这两个图纸直接是没有什么联系的,只不过都是图纸所以算A类的字节大小就单纯算A的。那当然了,不能单纯拿到B类的图纸。
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
class Solution
{
public:
int Sum_Solution(int n) {
cout << "Sum_Solution" << endl;
//...
return n;
}
};
int main()
{
A aa(1); //有名对象 -- 生命周期在当前函数局部域
A(2); //匿名对象 -- 生命周期在当前行
Solution().Sum_Solution(10); //匿名对象 -- 生命周期在当前行
const A& ra = A(1);//延长生命周期
return 0;
}
匿名对象具有常性,生命周期只在当前行,而const引用延长了匿名对象的生命周期,生命周期在当前函数局部域中。
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 Func1(A aa)
{}
A Func5()
{
A aa;
return aa;
}
int main()
{
A ra1 = Func5(); // 拷贝构造+拷贝构造 ->优化为拷贝构造
cout << "==============" << endl;
A ra2;
ra2 = Func5();
//A aa1;
//Func1(aa1); // 不会优化
//Func1(A(1)); // 构造+拷贝构造 ->优化为构造
//Func1(1); // 构造+拷贝构造 ->优化为构造
//A aa2 = 1; // 构造+拷贝构造 ->优化为构造
return 0;
}
现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:
类和对象就此告一段落,但是我们是需要不断学习新的知识的,本章节类和对象讲解了什么是类 – 封装在class中,什么是对象 – 我们的图纸的概念,什么是对象实例化 – 我们依靠这个图纸进行创造一个实例化的东西,那当然有诸多细节,是需要不断归纳总结的。
家人们不要忘记点赞收藏+关注哦!!!