C++如何重载指针的比较符
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++如何重载指针的比较符相关的知识,希望对你有一定的参考价值。
比如我有一个CLASS A,里面有很多成员,现在我用一个容器存了很多个A的指针,就比方是set吧,大家都知道,set里面存的元素必须是重载过小于号的,由于我存的是A的指针,不是A本身,那么想对成员变量x的值来进行排序怎么重载小于号?
PS:
bool operator < (const class* a, const class* b)
return a->x < b->x;
这种写法是不行的。
#include <iostream>
#include <set>
// 假设这是你的 A 类
class A
public:
A(int y) : x(y)
int x;
;
// 自定义一个比较 A 类指针的类型
class compareA
public:
// 重载括号操作符,自定义实现比较操作,示例如下:
bool operator () (const A* left, const A* right) const
if (nullptr == left)
return true;
else
return (left->x < right->x);
;
// 注意:在定义以A类指针为元素类型的set的时候
// 需要指定你的自定义比较操作类型
// std::set<T1, T2, T3> 的第二个模板类型就是比较类型
typedef std::set<A*, compareA> setpa_t;
int main()
A a(9), b(6), c(7), d(1);
setpa_t sa;
sa.insert(&a);
sa.insert(&b);
sa.insert(&c);
sa.insert(&d);
// 这里输出的值将是排好顺序的
for (setpa_t::iterator iter = sa.begin(); iter != sa.end(); ++iter)
std::cout << (*iter)->x << std::endl;
return 0;
参考技术A bool override operator < (const class* b)
return x < b->x;
追问
你的意思是这样吧?
class A
...
bool operator x;
;
这样不行,这样编译可以通过,但是这种写法的本质是重载了class与class*的比较,我这样试过,set里面的元素根本就没有排序,关键是要改变指针之间的重载。
用override覆盖基类的
C++ 类和对象
文章目录
对面向对象(OOP)的初步认识
- C语言是面向过程 的,关注是处理数据的过程,分析出求解问题的步骤,通过函数调用逐步解决问题。数据 和 处理数据的方法是分离的。
- C++是 基于面向对象 的,关注的是 对象 ,将一件事情拆分成不同的对象,靠对象之间交互完成。而C++ 将数据 和 处理数据的方法封装在一起,包含数据完整的生命周期。 但是C++也不是纯面向对象的语言,C++由于向下兼容C语言使得其也有面向对象的特性。
类的引入
C语言中我们学过 一种自定义类型——结构体 (可以参考博文:自定义类型详解)
C语言中结构体是一种数据类型,可以表示不同数据类型的一种集合,在C++中对struct的作用进行了延申,struct 里面不仅可以定义 数据 还可以定义函数!
struct Student
{
void SetStudentInfo(const char* s, int a)
{
strcpy(name, s);
age = a;
}
void print()
{
cout << name << " " << age << endl;
}
char name[20];
int age;
};
int main()
{
Student s;
s.SetStudentInfo("Peter", 20);
s.print();
return 0;
}
这里就要引出我们在C++中更喜欢用class来代替struct来表示这种新的数据类—— 类
类的定义
由上面的引入 得出了 组成类的成员:
- 类中的数据——成员变量
- 类中的函数——成员函数
下面是类的定义方式:
class classname
{
类的体:由成员函数 和 成员变量 组成
}; 注意这里的分号
class ——定义类的关键字 ,classnaeme——类的名字,{}里包含的是类的主体。
类的定义方式:
- 1.(函数)声明和定义放在一起(这里的定义指的是函数,成员变量在类中只是声明!)
class Student
{
void SetStudentInfo(const char* s, int a)
{
strcpy(name, s);
age = a;
}
void print()
{
cout << name << " " << age << endl;
}
char name[20];
int age;
};
注意: 这里在类里面定义函数 编译器一般会把其当成内联函数处理,所以小一点的函数可以在内里面定义,但是大一点的函数定义和声明还是分离比较好
- 2.(函数)声明和定义分开
可以声明在头文件 Student.h中
class Student
{
public:
void SetStudentInfo(const char* s, int a);
void print();
char name[20];
int age;
};
定义放在 Student.cpp中,使用::
运算符
#include<Student.h>
void Student::SetStudentInfo(const char* s, int a)
{
strcpy(name, s);
age = a;
}
void Student::print()
{
cout << name << " " << age << endl;
}
类的访问限定符及封装
访问限定符
说明:
- public修饰的成员可以被类外直接访问
- proteced和private修饰的成员在在类外不可以直接被访问
- 访问权限的作用域 是从该访问限定符出现的位置到下一个访问出现的位置之前
- class的默认访问权限 为private,struct为public(因为要兼容C,这也是class和struct在表示类时的唯一区别)
问题:class和struct 有什么区别?
在C语言中struct可以表示成结构体去使用。C++由于兼容了C语言的特性,所以struct既能表示结构体,又能表示类并且和class作用一样,唯一不同的是:class的默认访问权限 为private,struct为public
封装
首先我们要了解一下面向对象的三大特性:封装、继承、多态
什么是封装呢?
封装是将数据和操作数据的方法进行有机的结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
说大白话:就是想让你访问的就是公有,不想让你访问的就设成私有,你必须通过成员函数才能与数据交互
总结
封装实际上是一种对数据的管理,防止乱访问数据造成的修改
类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用::
作用域解析符指明成员属于哪个类
class Student
{
public:
void SetStudentInfo(const char* s, int a);
void print();
char name[20];
int age;
};
//这里要指明setStudentinfo是来自Student这个类域
void Student::SetStudentInfo(const char* s, int a)
{
strcpy(name, s);
age = a;
}
void Student::print()
{
cout << name << " " << age << endl;
}
类的实例化
类创建对象的过程,称为类的实例化
- 类只是一个模型,和struct表示的结构体一样是一个类型集合,定义一个类并没有实际给其分配内存空间来储存
- 一个类可以实例化多个对象,对象是类似于定义的变量,占有内存
打个比方:一个类定义出来就类似于一个图纸,类实例化出的对象就类似于按照图纸造出的房子,有了图纸你就可以造出房子,但图纸并没有实际存在的房子
class Student
{
public:
void SetStudentInfo(const char* s, int a);
void print();
char name[20]; //变量声明 不开辟空间
int age;
};
void Student::SetStudentInfo(const char* s, int a)
{
strcpy(name, s);
age = a;
}
void Student::print()
{
cout << name << " " << age << endl;
}
int main()
{
Student s; // 实例化的对象
s.SetStudentInfo("Peter", 20);
s.print();
return 0;
}
类的大小的计算
如何计算一个类的大小呢?
- 解决这个问题首先要知道如何处理类中成员函数所占的空间
这里成员函数其实是不存储在类里的,而是存储在内存分区中的 代码区。代码区存的都是在编译后 代码转换成的指令。而类的实例化是在堆栈上开辟的空间,所以在计算内存中无需考虑成员函数的大小,只 需要考虑成员变量。 - 然后类的大小分配原则和结构体的一模一样——都是按照内存对齐
具体 可以参考博客:结构体的内存对齐规则
class Student
{
public:
void SetStudentInfo(const char* s, int a);
void print();
char name[20];
int age;
};
void Student::SetStudentInfo(const char* s, int a)
{
strcpy(name, s);
age = a;
}
void Student::print()
{
cout << name << " " << age << endl;
}
int main()
{
cout << sizeof(Student) << endl;
}
由内存对齐规则可以知道结果是24
特殊的类的大小
class A
{
public:
void f2();
};
class B
{};
int main()
{
cout << sizeof(Student) << endl;
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
}
这里两个类 A 和 B并没有成员变量,按照上面的计算 内存应该为0,但是这里 人为的规定其大小为1 ,这里给一个字节是为了占位,表示对象存在,但是不存储任何有效数据!
类成员的this指针
我们先定义一个Data类:
class Data
{
public:
void print()
{
cout << "year: " << _year << " month: " << _month << " day: " << _day << endl;
}
void SetDate(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1;
Data d2;
d1.SetDate(2021, 10, 12);
d2.SetDate(2020, 10, 12);
}
对于上述类,有一个问题:
Data类中由SetData与Display两个成员函数,函数体中没有关于不同对象的区分,那当d1调用SetData函数时,该函数是如何区分应该设置d1对象还是设置d2对象呢?
C++中通过引入this指针来解决这个问题,C++编译器给每个非静态的成员函数增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作都是自动、隐式的,不用用户主动去调用。
this指针的特性
- this指针的类型:
类 类型 * const this
- 只能在 成员函数 内部使用
- this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不储存this指针
- this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
,不过你现实的添加this也行。
所以这里d1.SetDate(2021, 10, 12);
这句函数调用也就相当于 SetDate(&d1,2021,10,12);
注意:
- this指针存储在哪里?
this指针不是存储在对象里面!!this指针是形参,形参和函数中的局部变量都是存储在函数栈帧里面的,实际上是由ecx寄存器传入 - this指针不能为空
下面这段代码能让我们更深刻的了解 成员函数 和 this指针
class A
{
public:
void printA()
{
cout << _a << endl;
}
void show()
{
cout << "show()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->printA(); //语句1
p->show(); //语句2
}
问题:
- 这段代码能通过编译吗?能正常运行吗?
- 单独运行语句2 能正常运行吗?
首先这段代码是可以通过编译的,但是会在运行阶段挂掉,且中断在printA函数的_a
调用上。
一部分同学会认为编译无法通过的原因是 :p是一个空指针,对空指针解引用调用函数不是瞎搞吗?所以这里无法通过编译。这是对成员函数的存储位置还不是很了解,前面讲过成员函数是不储存在对象里面的,而是储存在内存上的代码区上,这里调用成员函数不回去访问p指向的空间,也就不存在对空指针解引用。
实际上:这里进行的操作是把 p(NULL)的值传给this指针!!
后面的现象也就很好解释了,传给this指针 show函数并没有调用类里面的变量,而printA函数调用了变量_a
,而我们知道实际上这里调用的是this->_a
,所以这里才出现了对空指针的解引用。
类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写任何东西的情况下都会默认生成 ——6个默认成员函数
注意:
- 默认成员函数在类定义之时,就会生成,但是如果自己定义了就不会再生成
- 默认成员函数也不是必须自己写,当默认成员函数能完成功能就不用自己写,如果不能完成功能 例如下面会讲到的 stack类,构造、析构、拷贝构造、赋值重载都要自己写!
构造函数
class Data
{
public:
void print()
{
cout << "year: " << _year << " month: " << _month << " day: " << _day << endl;
}
void SetDate(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1;
Data d2;
d1.SetDate(2021, 10, 12);
d2.SetDate(2020, 10, 12);
}
前面在实现日期类的时候,写过一个函数是void SetDate(int year, int month, int day)
这个函数的主要目的是对成员变量进行初始化,这里其实的功能和构造函数一模一样,都是对类成员变量进行初始化,但是缺点也是很明显定义与初始化是分离的,每次定义完要调用函数初始化。构造函数直接在定义的时候就可以初始化了。
-
构造函数——名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次。
-
默认构造函数——不用传参就可以调用的构造函数
特性
注意构造函数是初始化对象,而不是定义构造函数(开辟空间)
其特征如下:
- 函数名与类名相同
- 无返回值
- 对象实例化时编译器自动调用对应的构造函数
- 构造函数可以重载——提供多种初始化对象的方式。
class Data
{
public:
Data() //无参的构造函数
{
;
}
Data(int year , int month , int day ) //带参数的构造函数
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1;
Data d2(2021,7,26);
Data d3(); //注意无参构造函数初始化对象时,对象后面不用跟括号,否则就成函数声明了
//这里就是一个无参返回值是类Data的函数名为d3的函数
}
- 如果没有显示的定义构造函数,则C++编译器会自动生成一个无参默认构造函数,一旦用户显示定义编译器就不会再生成
class Data //这里没有定义构造函数,编译器就会自己生成一个
{
private:
int _year;
int _month;
int _day;
};
那么编译器生成的构造函数能完成什么功能呢?
- 对待内置类型例如:int ,double ,指针,long…默认构造函数是不会初始化的
- 对待自定义类型(class、struct)会调用它的 默认 构造函数(不用传参数的构造函数)
class A
{
public:
A(int a = 100)
{
_a = a;
}
int _a;
};
class Data
{
void print()
{
cout << ps._a <<endl<<_year<<endl<<_month<<endl<<_day<< endl;
}
private:
int _year;
int _month;
int _day;
A ps;
};
int main()
{
Data d1;
d1.print();
}
这里在类Data成员变量中定义了 三个int类型和一个类A类型的变量,最后只有自定义类型的变量被初始化了
。
- 无参构造函数和全缺省构造函数 都称为默认构造函数,并且默认构造函数只能有一个! 注意:无参构造函数、全缺省构造函数、编译器自己生成的构造函数 统称为 默认构造函数。
误区: 只有编译器自己生成的构造函数才是默认构造函数
class Data
{
public:
Data()
{
_year = 0;
_month = 1;
_day = 1;
}
Data(int year=0 , int month=1 , int day=1 ) //两个默认构造函数不能同时存在
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
不能存在两个默认构造函数,如果在类的实例化时不传参,编译器不知道调用哪一个构造函数。
总结:
大多数情况下构造函数都要自己去写,因为初始化出来的变量才会符合要求。一般情况下建议写一个全缺省的构造函数,这样可以应对各种场景。
参数列表
上面我们介绍了构造函数,其中有一种特殊的构造函数:默认构造函数。默认构造函数对待内置类型是不处理的,对待自定义类型是调用自定义类型的 默认构造函数 (注意是 默认构造函!!!!)
但是这里遗留了一个问题,如何对自定义类型里面的变量赋值?
class B
{
public:
B(int x=1,int y=2)
{
_x = x;
_y = y;
}
const B& operator=(const B& d1)
{
_x = d1._x;
_y = d1._y;
return *this;
}
private:
int _x;
int _y;
};
class A
{
public:
A(int a,int b,int c)
{
B b2(b,c);
b1 = b2;
// b1= B(b,c) 使用匿名对象:生命周期只有这一行!
_a = a;
}
private:
int _a;
B b1;
};
int main()
{
A a1(100,100,100);
}
如果想要把值赋给类A中的类B成员变量,只能先创建一个临时变量赋值,然后再用赋值运算符重载拷贝给成员变量。这样定义类中的 类对象会十分麻烦。
这里我们就要介绍一下初始化列表:
初始化列表: 以一个冒号开始,接着是以一个以逗号分隔的数据成员列表,每个“成员变量”后面跟一个放在括号中的初始值或表达式
A(int a,int b,int c) //未显示定义参数列表,但是参数列表依然存在
{
B b2(b,c);
b1 = b2;
_a = a;
}
A(int a,int b,int c)//使用初始化列表
:_a(a)
,b1(b,c)
{}
之所以参数列表能解决上面的问题,实际上是自定义类型会在参数列表处初始化(而自定义的 初始化 和 变量赋值是绑定在一起的),如果我们能在初始化的时候赋值就可以调用非默认构造函数了,而不是通过创建临时变量赋值重载。
参数列表不管你是否显示的写出来一直是存在的,而且一切变量都会在参数列表处初始化(如果我们对未显示定义参数列表的构造函数按f11一步一步的调试,会发现实际上在进入构造函数之前会跳到类B的默认构造函数)
注意:
-
每个成员只能在初始化列表上出现一次
-
类中包含以下成员必须在初始化列表处初始化:
- 引用成员变量
- const成员变量
- 自定义类型成员变量(该类没有默认构造函数)
可以发现必须在初始化列表初始化的成员变量有一个共性:定义的时候就必须赋初值,赋初值是和定义是绑定在一起的。
易错点
class A
{
public:
A(int x)
:_a1(x)
, _a2(_a1)
{}
void print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
int main()
{
A a(1);
a.print();
return 0;
}
结果:很多人会认为结果是 1 1,但实际上是:
成员变量在类中声明次序就是其在初始化列表中初始化顺序,与其在初始化列表中的先后次序无关。
析构函数
概念: 以上是关于C++如何重载指针的比较符的主要内容,如果未能解决你的问题,请参考以下文章
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时