C++学习:5其他语法
Posted 想文艺一点的程序员
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++学习:5其他语法相关的知识,希望对你有一定的参考价值。
目录
运算符重载
1、运算符重载(operator overload)
在 C++ 当中非常重要!!!!
运算符重载(操作符重载) :可以给运算符增加一些新的特性
举个例:我们想直接加俩个point,利用加号。
operator
再次理解:
汇编语言:
2、完善
运算符重载参数: const Point &p1
- 输入型参数
- 减少中间变量的产生
- 既可以接收 const 类型的参数,可以接收 非const 类型的参数
拷贝构造函数的参数如果是 对象类型
- 会造成无线调用构造函数,所以编译器报错。
- Point p1 = p2 , 无限调用。
拷贝构造函数的参数如果是 const Point &p1
- 输入型参数
- 减少中间变量的产生
- 既可以接收 const 类型的参数,可以接收 非const 类型的参数
完善运算符的特性:
改进一:将重载运算符的返回值,变为 const 类型
- 因为 p1 + p2 ,返回一个 const 类型的对象,只能调用,不能进行修改赋值。
缺点:
- 不能进行 连加 运算:
原因: const 类型的对象,不能调用 非const 类型的成员函数。(在 const 成员函数当中有分析)
改进二:将其改为 const 成员函数
3、更多运算符
之前我们的运算符重载,我们是写在了类外面,我们现在将他写到类里面。
优点:
- 可以少写一个参数,通过 this 指针来代替一个。
- 既然是成员函数,那么就可以访问类内的所有成员,不需要变为友元函数。
实现 减 的重载:
实现 += 运算符:
更改返回值类型: 更换为 引用
- 如果返回的是 对象类型,那么会产生中间对象, p3 就会赋值给中间对象。
实现 == 运算符,判断一下两个点是否相等。
4、单目运算符
单目运算是指运算符包括 算术运算符、逻辑运算符、位逻辑运算符、位移运算符、关系运算符、自增自减运算符。
实现 负号:
需求:我们只是想要 p1 的负数,并不想让 p1 本身发生改变。
- 返回一个临时对象,并不是返回引用。
缺点:赋值的时候不报错。
改进:将返回值更改为 const 类型, 则不能进行赋值。
缺点:不能连续使用负号,
原因:返回的 const 临时对象不能调用 非const 函数。
改进:将函数更改为 const 类型
实现 ++(自增) 运算符
5、cout 的重载
实现 << 运算符:(cout << p1 << endl;)
-
成员函数:只能控制右边是什么,左边已经固定为调用成员函数的对象。
-
全局函数:可以通过参数,来控制运算符的左边是什么,右边是什么。
改进:变为 Point 的友元函数,全局函数可以访问 Point 的成员函数。
实现连续打印:
- 缺点:换行符 已经写死到 << 里面。
改进换行符:将换行符 << 的控制权,更换到 << 函数外面。
6、cin 的重载
同理:
实现 >> 运算符:( cin>>a )
-
成员函数:只能控制右边是什么,左边已经固定为调用成员函数的对象。
-
全局函数:可以通过参数,来控制运算符的左边是什么,右边是什么。
我们的需求:从键盘输入一个坐标,赋值给 Point 对象。
-
cin 的类型为 input stream
-
右边的对象应该是 非const 类型。(输出型参数),通过键盘来进行更改
7、调用父类的运算符重载函数
-
继承之后,父类的私有成员,不能直接调用,但是可以通过调用父类的运算符重载即可。
-
返回值为引用:连续赋值
8、仿函数(函数对象)
仿函数:将一个对象当作一个函数一样来使用
- 将 ( ) 进行重载
- 对比普通函数,它作为对象可以保存状态
9、注意点
为什么有些运算符只能写在类里面呢?
- 这些运算符都是这个类特有的。
- 如果写成全局函数,那么就成了全局的重载。
#include <iostream>
using namespace std;
class Point {
//friend Point operator+ (const Point &p1, const Point &p2);
int m_x;
int m_y;
public:
int getX() { return m_x; }
int getY() { return m_y; }
Point(int x = 0, int y = 0) : m_x(x), m_y(y) {}
Point(const Point &p1) {
}
void display() {
cout << m_x << " " << m_y << endl;
}
const Point operator+ (const Point &p2) const {
return Point(m_x + p2.m_x, m_y + p2.m_y);
}
const Point operator- (const Point &p2) const {
return Point(m_x - p2.m_x, m_y - p2.m_y);
}
Point & operator+= (const Point &p2) { // 这里要将其修改为 非const 类型,因为里面要修改成员变量
m_x = m_x + p2.m_x;
m_y = m_y + p2.m_y;
return *this;
}
bool operator== (const Point &p2) {
if (m_x == p2.m_x && m_y == p2.m_y) {
return true;
}
else {
return false;
}
}
const Point operator-()const {
return Point(-m_x, -m_y);
}
Point &operator++() {
m_x++;
m_y++;
return *this;
}
const Point operator++(int) { // 语法糖,参数添加 int 就代表后置 ++
Point old(m_x, m_y);
m_x++;
m_y++;
return old;
}
Point &operator--() {
m_x--;
m_y--;
return *this;
}
const Point operator--(int) { // 语法糖,参数添加 int 就代表后置 ++
Point old(m_x, m_y);
m_x--;
m_y--;
return old;
}
friend ostream & operator<<(ostream &cout, const Point &point);
friend istream &operator>>(istream &cin, Point &point);
};
ostream &operator<<(ostream &cout, const Point &point) {
cout << "(" << point.m_x << "," << point.m_y << ")" ;
return cout;
}
// input stream ==== istream
istream &operator>>(istream &cin, Point &point) {
cin >> point.m_x;
cin >> point.m_y; // 以回车结尾
return cin;
}
int main()
{
getchar();
return 0;
}
模板
先来了解什么是泛型?
- 将类型参数化
- 达到代码复用的目的。
- C++ 当中使用 模板 来实现泛型。
先来看一个需求:实现一个加法运算
分析:
都是实现一个加法,但是需要写 3 个函数,很麻烦。
解决:
泛型:是一种将类型参数化以达到代码复用的技术, C++中使用模板来实现泛型。
1、函数模板
使用模板的目的:进行泛型编程。
格式:
语法糖:
理解什么是类型参数化:类型 也可以当作是一个参数传过去。
- T = int , T = double ,T = Point。
可以通过反汇编看出,add 本质调用的并不是同一个函数,而是 3 个函数。
本质:我们写 1 份函数,编译器帮我们生成 n 份函数。
注意:
1、模板没有被使用时,函数实体是不会被实例化出来的
2、当传入参数是不同类型的时候:
3、模板的声明和实现如果分离到.h和.cpp中,会导致链接错误,一般将模板的声明和实现统一放到一个 .hpp文件中。
之前的函数实现:
使用模板的函数实现:
报错:无法解析外部符号
通过下面的编译细节,我们来分析为什么会报错。
2、编译细节
正常函数的编译和链接
分析:模板的声明和实现如果分离到.h和.cpp中,会导致链接错误,为什么会报错?
1、分析哪些文件会被编译。
- .h 头文件不会参与编译,在预处理的时候被替换。
- .cpp 源文件会参与编译。
2、怎么进行编译呢?
- 第一步:每个 .cpp 源文件 单独进行编译。生成一个 obj 文件。
- 第二部:链接各个 obj 文件,从而生成一个 exe 文件。
查看生成的文件:
再次深入的分析流程:
-
add 函数的真正代码实体,在 math.obj 里面。
-
函数的调用,本质就是 call 函数地址 。(函数地址在 math.obj 里面,main.obj 里面并没有函数体)
问题:main 文件,怎么找到 call 函数地址 中的函数地址呢?
- 在 main.cpp 编译成 main.obj 的过程当中,call 当中的函数地址是瞎写的。(只是为了骗过编译器,真正地址在链接时候给)
- main.obj 和 math.obj 链接过程当中,
- 编译阶段: call 当中的函数地址是假的,因为 main.obj 文件并不知道 math.obj 文件存在。
- 链接阶段:修正 call 当中的函数地址,保证可以调用正确。
模板函数的编译和链接
- main.cpp 编译可以通过,不会报错,因为 call 当中的函数地址也是假的。
- math.cpp 编译可以通过,不会报错,但是 math.obj 当中并不会有函数实体生成。
- 因为 math.cpp 并不知道 main.cpp 的存在,不知道传入了什么样的参数类型,所以也不会进行实例化。
所以一般将:一般将模板的声明和实现统一放到一个**.hpp文件**中 (hpp 文件 == h文件 + cpp文件)
- hpp 文件并不参与编译
- 本质还是只有 main.cpp 进行编译,函数实体也生成在 main.obj 当中。
3、类模板
引出一个案例:在 C 语言当中的数组,数组在定义之后大小是固定的,不能进行更改。
需求:我们想要一个动态数组,大小可以动态进行改变。
分析:
1、我们的数组应该放在栈空间?还是堆空间?
答:堆空间,因为堆空间的申请和释放是动态的,可以根据程序员需求进行更改。
栈空间却是确定死的,由系统决定。
2、怎么实现动态扩容?
- 申请一个更大的堆空间。
- 将前面的数据拷贝过去。
分析为什么?
新 new 一个int 可以吗?
- 发现前面的堆空间丢了,造成了内存泄漏,并且前面的数据,我们也不能访问了。
再使用一个新的指针?
- 根本就不是一个数组了,数据也不连续,数组名也不同。
- 并且非常浪费栈空间,我们每添加一个数据,就需要一个新的指针。
解决办法:
- 申请一个更大的堆空间。
- 将前面的数据拷贝过去。
- 然后将旧的堆空间,进行 delete 。
再来分析我们动态数组类当中,需要那些成员?
- 指向堆内存的指针
- 目前数组里面有多少元素
- 初始化最多可以放多少数组。
缺点:这个数组的数据比较单调,现在只能放 int 。
-
我们希望可以存放,char、Point 、double 、等等其他的数据。
-
使用类模板
怎么使用类模板 :
同理:类模板也可放在 hpp 文件当中。
4、完善类模板
类型转换
铺垫:
1、C 语言风格的类型转换符
- (type)expression
- type(expression)
int a = (int) 10.5;
2、C++中有4个类型转换符
- static_cast
- dynamic_cast
- reinterpret_cast
- const_cast
使用格式: xx_cast<type>(expression)
int a = 10;
double b = static_cast <double>(a);
double b = dynamic_cast <double>(a);
double b = reinterpret_cast <double>(a);
double b = const_cast <double>(a);
注意:这些强制转换比较复杂
1、const_cast
一般用于去除const属性,将const转换成 非const
理解:本质其实就是为了骗过编译器,使代码可以编译通过。
- p1 的本质还是 const Person 类型, p2 的本质就是 Person 类型。
- 因为类型不同,不能直接进行赋值操作,所以需要进行类型转换。
- 类型转换之后,p1 和 p2 的本质还是不变的。
对比一下,c语言的转化 和 c++ 转换:
- 可以发现,根本没有区别。
- 本质的汇编都是一样的,都是用来骗过编译器。
2、dynamic_cast
- 一般用于多态类型的转换,有运行时安全检测
多态本质:实现父类与各个子类之间的多态。(所以必定要涉及继承)
来看一下用法:
修改:将 Person 类 和 Student 类 变为多态类。
区别一下 C 语言的转换:可以明显看出是不一样的
- C++ 的 dynamic_cast 多了一个安全检测
怎么理解安全检测呢?
- 可以看出,当我们使用 子类指针指向父类指针 的时候, dynamic_cast 转换的指针直接将他们变为 0000000.
第二种安全检测:
- 两个毫不相关的类不是赋值。
3、static_cast
- 对比dynamic_cast,缺乏运行时安全检测
- 不能交叉转换(不是同一继承体系的,无法转换,这点和 dynamic_cast 类似)
- 常用于基本数据类型的转换、非const转成const
- 使用范围较广
不能交叉转换(不是同一继承体系的,无法转换,这点和 dynamic_cast 类似)
对比dynamic_cast,缺乏运行时安全检测
4、reinterpret_cast
我们常用一般只有两个:
- dynamic_cast : 因为它有安全检测。
- reinterpret_cast :属于比较底层的强制转换
- 属于比较底层的强制转换,没有任何类型检查和格式转换,仅仅是简单的二进制数据拷贝
- 可以交叉转换
- 可以将指针和整数互相转换
分析存储方式:
x86 是小端模式:
- int a = 10;
- 低地址 ————> 高地址: 0a 00 00 00
- 读取变量 a 的时候:先读取高地址的值。
继续分析 :double = 10 的内存存储:
-
可以看出浮点数的存储方式,和我们的认知不一样。
-
double 是 8 字节,我们可能本能以为是: 0x 0a 00 00 00 00 00 00 00
思考:int 强制转化为 double 类的时候:
- 并不是简简单单的二进制拷贝
如果我们想要进行二进制拷贝,怎么办呢?
解决: 使用 reinterpret_cast 转换即可。(不会进行任何类型转换,直接进行二进制拷贝)
以上是关于C++学习:5其他语法的主要内容,如果未能解决你的问题,请参考以下文章