作者:@小萌新
专栏:@C++初阶
作者简介:大二学生 希望能和大家一起进步!
本篇博客目标:梳理类和对象剩下几个零碎的知识点
专注是一种能力
相比较于C语言中的输入输出函数来说 C++的输入输出流不用指定类型
这是为什么呢? 因为做了什么神奇的优化吗?
其实并不是 这里只是用到了我们之前学过的一种东西而已 它的名字叫 函数重载
当然 这些重载函数能够识别的类型仅限于内置类型 对于我们的自定义类型来说 识别是不可能的
所以说我们这里就需要对于流插入和流提取运算符进行一个重载
而对于流来说 其中有输入流和输出流 也就是我们平时经常说的io流
而对于流插入和流提取来说 它们对应的分别是 istream 和 ostream
下面我们来展示下应该怎么重载流插入运算符
void operator << (ostream& out)
{
printf("<<");
}
我们来试试看
可以完美运行
但是如果我们这样子操作呢?
我们发现这样子竟然不能运行了
这是为什么呢?
还记不记得我们之前讲的this指针
我们是不是发现 this指针在前面 流插入在后面啊
那么我们试试这样子
但是这样子操作是不是有点奇怪啊
那么 既然我们知道了为什么会出现这种错误 是不是就知道怎么修正了
void operator << (ostream& out, MyClass& d)
{
printf("<<");
}
我们将它按照我们需要的参数顺序定义到类的外面是不是就可以了
我们来看看能不能运行
答案是可以的
但是假设我们需要打印类内部的成员变量在外面可以打印嘛?
显然是不行的
因为成员变量都被外面私有了 外面是无法访问的
那应该要怎么做呢?
我们这里想到的第一个处理方式就是将成员变量全部公有化
但是这样就失去了我们写类的意义了
这里试验证明下
那么除了公有化之外我们还有什么处理方式呢? 这里就引出了我们很重要的一个概念 友元
我们先来看看怎么使用
friend void operator << (ostream& out, MyClass& d);
在类的任意位置使用 friend 关键字 在类中的任意位置声明改函数为类的友元函数(可以访问类里面的私
有)
我们将全部类成员变量私有化然后声明友元函数试试看
我们发现可以完成
当然友元函数最好不要过多声明 这样子会造成函数之间耦合度过高
我们写程序尽量要符合 高内聚 低耦合
上面我们解决了流插入操作符的重载 并且解决了类私有成员变量的问题
同样的这里我们对于流提取再写一个重载函数
代码表示如下
istream& operator >> (istream& in, MyClass& d)
{
in >> d._a;
return in;
}
我们可以发现 流提取操作也能完美运行
在创建对象的时候 编译器通过调用构造函数 给对象中的各个成员变量赋一个初始值
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
比如说这样子
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
简单来说什么意思呢
就是说只有第一次的赋值叫做初始化 构造函数里面只是赋初值而已
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个**"成员变量"后面跟一个放在括号中的初始值或表达式**。
我们直接来看看代码是怎么写的
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{}
那么既然构造函数里面可以初始化 我们为什么还需要学列表初始化呢?
事实上我们在上面已经说过了构造函数里面并不是初始化 而是赋初值
也就是说如果我们在上面就已经初始化了
具体是什么时候呢? 就是列表初始化的时候
那么对于一些只能在初始化的时候赋值的变量 我们就只可以使用列表初始化
有这么几种情况比如要用到列表初始化
class A
{
public:
A(int a, int b)
:_a(a)
,_b(b)
{}
private:
int _a;
int _b;
};
class B
{
public:
B(int a, int ref)
:_aobj(a,ref)
,_ref(ref)
,_n(10)
{}
private:
A _aobj; // 没有默认构造函数
int& _ref; // 引用
const int _n; // const
};
void Test()
{
B b(10,1);
}
就比如说我们上面的代码 就是列表初始化使用的场景
假如我们不使用列表初始化 便会发生下面这三个错误
因为初始化会优先使用列表初始化
后面的构造函数其实就没必要使用了
我们来看看上面的话对不对
这句话怎么理解呢?
我们直接来看代码
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();
}
这里是什么呢?
因为a2是首先声明的 所以说这里先对a2进行初始化 而这个时候a1的值是随机值 所以说a2就变成了随机值
而a1是后声明的 a的值传参为1 所以说这个时候a1的值就变成了1
通过这个题目大家应该可以理解 成员变量在类中声明次序就初始化次序 这句话的含义了
学习了初始化列表之后并不是说我们以后就一定要用初始化列表了
可以将初始化列表和构造函数混着用 灵活变通 因地制宜
在了解这个关键字之前我们首先要了解下隐式类型转换
我们首先来看下面这段代码
class Date
{
public:
Date(int year)
:_year(year)
{}
void Print()
{
cout << _year << endl;
}
private:
int _year;
};
int main()
{
Date d1(10);
Date d2 = 10;
return 0;
}
这里的d2是一个自定义类型是吧
这里的10是一个整形是吧
但是我们竟然可以将10赋值给d2
这中间发生了什么呢?
这里就出现了隐式类型转换
接下来我们再看以下代码
class Date
{
public:
Date(int year)
:_year(year)
{}
void Print()
{
cout << _year << endl;
}
private:
int _year;
};
int main()
{
Date d1(10);
Date d2 = 10;
Date& d3 = 10;
return 0;
}
我们这里可以发现 引用类型为啥就不行了呢?
这是因为啊 右边是常数 是只读类型的 但是引用是可读可写的所以说这是不是设涉及到权限的放大缩小
问题了啊 这个时候我们就需要在前面加上一个const
这样子就可以啦
之后我们来看看 explicit 关键字的语法
我们将explict关键字放到Date构造函数前面 之后再运行下程序试试
我们可以发现 这里不能进行隐式类型转换了
关于这个有什么作用 我们后面再讲 这里只要记住到这一点
被explict修饰构造函数之后便无法进行隐式类型转换了
我们都知道 拷贝函数和构造函数都会创建/拷贝对象
所以说是不是我们往里面放进去一个计数器就可以了啊
我们来看这个类
class Sum
{
public:
Sum()
{
_i++;
}
Sum(const Sum& d)
{
_i++;
}
void Print()
{
cout << _i << endl;
}
private:
static int _i;
};
假设我们想知道它们拷贝 构造了多少次 是不是直接调用下Print就可以
我们来看看
这样子我们就知道了 它调用了三次构造或者是拷贝
这里有一点比较关键的就是static int _i 的初始化
这里记住 一定要再类的下面 使用
int Sum :: _a = 0;
这样子初始化
当我们使用static关键字修饰一个参数的时候 实际上这个参数就被放到静态区去了
所以说此时它的生命周期应该是整个程序运行时
但是呢 它们的使用范围却不同
这里总结下
全局变量 : 生命周期是全局,可以随意访问调用
局部变量 :生命周期是全局,在局部变量使用,局部变量外不能使用
类中定义 :生命周期是全局,类只起到限定域的作用 (相当于类是一个命名域)
再一句话总结下 什么意思呢?
虽然它们的生命周期变成了整个程序运行时
但是它们的使用空间却是没有变化的
当static修饰函数的时候 这个函数也会被放到静态区
当放到静态区之后也就没有this指针了
没有this指针说明什么呢
我们就不能访问内部成员变量了
我们来看看代码
这里为什么还能访问_i呢 因为它是一个被static修饰的成员变量 实际上储存在静态区
实际上我们只要换成_year就不能打印了
类似于这样子
意区分两个问题:
1、静态成员函数可以调用非静态成员函数吗?
2、非静态成员函数可以调用静态成员函数吗?
问题1:不可以。因为非静态成员函数的第一个形参默认为this指针,而静态成员函数中没有this指针,故静态成员函数不可调用非静态成员函数。
问题2:可以。因为静态成员函数和非静态成员函数都在类中,在类中不受访问限定符限制
这里就类似于权限的放大缩小问题
我们再上面的输入输出流中已经介绍过友元
这里再详细介绍下
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
前面我们解决私有成员变量问题就是用友元函数去解决
中间的详细解释参考上面的代码
这里给出友元函数的说明
友元函数说明:
1、友元函数可以访问类是私有和保护成员,但不是类的成员函数。
2、友元函数不能用const修饰。
3、友元函数可以在类定义的任何地方声明,不受访问限定符的限制。
4、一个函数可以是多个类的友元函数。
5、友元函数的调用与普通函数的调用原理相同
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中非公有成员。
class A
{
// 声明B是A的友元类
friend class B;
public:
A(int n = 0)
:_n(n)
{}
private:
int _n;
};
class B
{
public:
void Test(A& a)
{
// B类可以直接访问A类中的私有成员变量
cout << a._n << endl;
}
};
我们再A中声明了B是A的友元类 那么这个时候B就可以访问A的所有私有内容了
友元类说明:
1、友元关系是单向的,不具有交换性。
例如上述代码中,B是A的友元,所以在B类中可以直接访问A类的私有成员变量,但是在A类中不能访问B类中的私有成员变量。
2、友元关系不能传递。
如果A是B的友元,B是C的友元,不能推出A是C的友元.
代码表示如下
class A
{
private:
static int k;
int h;
public:
class B // B天生就是A的友元
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a.h << endl;//OK
}
};
};
int A::k = 1;
int main()
{
A::B b;
b.foo(A());
A a;
cout << sizeof(a) << endl; // 4
return 0;
}
这里我们形象的解释下
A将B捧在手心里 所以说A是B的舔狗
所以说 A对B来说 随叫随到 所以B能够随时访问A的所有私有内容
但是对于B来说 A只是以一个舔狗 所以说 A不可以访问B的私有内容
之后我们再来讲一下输出的类A大小 是4
说明了什么? 说明类的大小与其中的内部类无关
前面的博客中已经说了 封装的本质是一种管理
就拿疫情来说
你觉得是放任自由 按照往常一样生活对于疫情防控更加有效
还是每天核酸 封闭式管理对于疫情防控更加有效呢?
大家可以看看这张图
类是抽象的 对象是实际存在的
就拿我们的祖师爷 冯 诺依曼提出的计算机理论来说
计算机要有 输入 输出设备 要有 储存器 运算器和控制器
这就是一个类
然后大家现在面前的计算机就是由这个类实例化出来的一个对象
本篇博客主要梳理了类和对象剩下的一些知识点
好累啊! 类和对象的三篇内容一篇比一篇折磨人 好在终于要结束了 马上就要进入愉快的STL学习时间啦
由于博主的水平错误再说难免 希望大佬发现后能够及时指正
如果本篇文章有帮助到你 别忘了一键三连啊
阿尼亚 哇酷哇酷!