对面向对象(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;
};
注意: 这里在类里面定义函数 编译器一般会把其当成内联函数处理,所以小一点的函数可以在内里面定义,但是大一点的函数定义和声明还是分离比较好
可以声明在头文件 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;
};
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();
p->show();
}
问题:
- 这段代码能通过编译吗?能正常运行吗?
- 单独运行语句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();
}
- 如果没有显示的定义构造函数,则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;
_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++编译器会自动调用析构函数
class stack
{
public:
stack(int capacity=4)
{
a = (int*)malloc(capacity * sizeof(int));
assert(a);
_capacity = capacity;
_sz = 0;
}
~stack()
{
if (a)
{
free(a);
a = NULL;
_capacity = 0;
_sz = 0;
}
}
private:
int* a;
int _capacity;
int _sz;
};
- 关于编译器自动生成的默认析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,会调用自定义类型的析构函数
class String
{
public:
String(const char *str="jack ma")
{
p = (char*)malloc((strlen(str)+1) * sizeof(char));
strcpy(p, str);
}
~String()
{
if (p)
{
free(p);
p = NULL;
}
}
private:
char* p;
};
class Person
{
public:
Person(int age = 20)
{
_age = age;
}
private:
int _age;
String name;
};
int main()
{
Person a1;
return 0;
}
我们按下f10打开调试界面
拷贝构造函数
概念: 拷贝构造函数:在对象创建的时候,将另一个对象的全部内容复制给该对象
特征:
- 拷贝构造函数是构造函数的一种重载
- 拷贝构造函数的参数只有一个且必须引用传参,使用传值的方式会引发无穷递归
class Data
{
public:
Data(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Data(const Data& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
为什么不能使用传值传参? 传值在传参的过程中要拷贝给临时变量,这个过程又要调拷贝构造,而拷贝构造又是传值传参…
- 若未显示定义,系统生成默认的拷贝构造函数。默认拷贝构造函数按对象字节序拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。
这种拷贝和memcpy函数的拷贝实现是相同的
class Data
{
public:
Data(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << endl << _year << endl << _month << endl << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(2021, 1, 1);
Data d2(d1);
d2.print();
return 0;
}
- 那么编译器生成的默认拷贝构造函数已经可以完成字节序的拷贝,我们还需要自己实现拷贝构造函数吗?上面的Data日期类似乎并不需要,但是有些类就需要了
class String
{
public:
String(const char *str="jack ma")
{
p = (char*)malloc((strlen(str)+1) * sizeof(char));
strcpy(p, str);
}
~String()
{
if (p)
{
free(p);
p = NULL;
}
}
private:
char* p;
};
int main()
{
String s1("love");
String s2(s1);
return 0;
}
这个程序如果运行就会崩溃原因如下: 由于浅拷贝使得s1.p和s2.p指向堆上同一块空间,在作用域结束时候调用析构函数时,由于s2后创建先析构,s2.p所指向的空间还给了操作系统,但是s1析构的时候,s1.p就会重复释放已经被释放的空间,所以会报错! 这里就体现了浅拷贝的坏处了,浅拷贝并没有拷贝实际的内存,而是拷贝了指向那块内存的地址!
赋值操作函数
运算符重载
内置类型是支持运算符的,但是自定义类型是不支持的,运算符重载使得自定义类型也能使用操作符
C++为了增加代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似
函数名为: 关键字operator后面接需要重载的运算符符号
函数原型: 返回值类型operator操作符(参数列表)
注意:
- 不能通过连接其他符号来创建新的操作符 例如
operator @ - 重载操作符必须有一个类类型或者枚举类型的操作数(内置类型不能重载,参数类型不能都是内置类型!)
当运算符有两个操作数时,第一个参数是左操作数,第二个参数是右操作数
- 用于内置类型的操作符,其含义不能改变,例如:内置的整形+,不能改变其含义
- 作为类成员的重载函数,其参数看起来比操作数数目少1,成员函数的操作符有一个默认的形参this,限定为第一个形参
- 运算符重载函数一般作为成员函数放在类里面,如果在类外定义,就无法访问类里面的private成员
.* 、 . 、 :: 、 ?: 、 sizeof 注意以上五个运算符不能重载
class Data
{
public:
Data(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Data(const Data& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
bool operator==(const Data d)
{
if (_year == d._year && _month == d._month && _day == d._day)
return true;
else
return false;
}
void print()
{
cout << endl << _year << endl << _month << endl << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(2021, 10, 12);
Data d2;
Data d3(d1);
cout << (d1 == d2) << endl << (d1 == d3) << endl;
return 0;
}
赋值运算符重载
我们通过上面的学习可以很简单的写出赋值运算符重载函数:
class Data
{
public:
Data(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Data(const Data& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void operator=(const Data& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void print()
{
cout << endl << _year << endl << _month << endl << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
但是这里的赋值运算符是不完全正确的,在一些情况下例如:连续赋值的时候就无法使用 赋值运算符主要有四点:
- 参数类型,参数个数只有一个一般为const类
- 返回值,返回值一般返回引用,减少返回时的拷贝
- 检测是否给自己赋值
- 返回*this
- 一个类如果没有 显示的定义赋值重载运算符,编译器会生成一个按字节序拷贝的赋值重载
上面编写的赋值重载每有考虑到 例如d1=d2=d3; 这种连续赋值的情况,下面做出改进:
class Data
{
public:
Data(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Data(const Data& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
bool operator==(const Data& d)
{
if (_year == d._year && _month == d._month && _day == d._day)
return true;
else
return false;
}
Data& operator=(const Data& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
void print()
{
cout << endl << _year << endl << _month << endl << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(2021, 10, 12);
Data d2;
Data d4;
d2 =d4= d1;
d4.print();
d2.print();
return 0;
}
使用引用返回的原因:首先是这里this指针传进来的是类的地址,所以*this指向的就是主函数里面定义的类的空间,而我们知道在传值返回的时候,是先将返回值传给一个中间变量(调用拷贝构造函数),再将中间变量传回主函数(赋值重载)。但是这里用传引用就不用拷贝,而且引用的变量由于是全局变量不会出函数就会被销毁。
默认拷贝构造与赋值运算符重载的问题
很多人学到这有一个问题:赋值运算符重载和 拷贝构造函数实现的内容都差不多,为什么还要学习赋值重载运算符? 搞清楚两个不同点:
-
重载的对象不同: - 拷贝构造函数的重载指的是函数的重载,对构造函数的重载 - 赋值运算符的重载指的是运算符的重载,对运算符的重载 -
实现的对象不同: 拷贝构造函数 是在类变量初始化的时候调用的,而拷贝运算符是对两个已经初始化过的类变量使用的,且注意:Date d1=d2; 这种情况不是 赋值 而是 拷贝构造!
实现一个日期类
下面运用上面的知识实现一个日期类: Date.h文件
#include<iostream>
#include<assert.h>
using namespace std;
class Date
{
public:
int GetMonthDay(int year, int month);
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d);
Date& operator=(const Date& d);
Date& operator+=(int day);
Date operator+(int day);
Date operator-(int day);
Date& operator-=(int day);
Date& operator++();
Date operator++(int);
Date operator--(int);
Date& operator--();
bool operator>(const Date& d)const;
bool operator==(const Date& d)const;
inline bool operator >= (const Date& d)
{
return *this - d >= 0;
}
bool operator < (const Date& d)const;
bool operator <= (const Date& d)const;
bool operator != (const Date& d)const;
int operator-(const Date& d);
void print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
Date.cpp文件
#include"Date.h"
int Date::GetMonthDay(int year, int month)
{
static int a[12] = { 31,0,31,30,31,30,31,31,30,31,30,31 };
if (month == 2)
{
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
return 29;
else
return 28;
}
else
return a[month-1];
}
Date::Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date& Date::operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
Date& Date::operator+=(int day)
{
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month > 12)
{
_year++;
_month = 1;
}
}
return *this;
}
Date Date::operator+(int day)
{
Date d1(*this);
d1 += day;
return d1;
}
Date Date::operator-(int day)
{
Date d1(*this);
d1 -= day;
return d1;
}
Date& Date::operator-=(int day)
{
_day -= day;
while (_day < 0)
{
_day += GetMonthDay(_year, _month);
_month--;
if (_month < 0)
{
_year--;
_month = 12;
}
}
return *this;
}
Date& Date::operator++()
{
_day += 1;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month > 12)
{
_year++;
_month = 1;
}
}
return *this;
}
Date Date::operator++(int)
{
Date d1(*this);
++(*this);
return d1;
}
Date Date::operator--(int)
{
Date d1(*this);
--(*this);
return d1;
}
Date& Date::operator--()
{
_day -= 1;
while (_day < 0)
{
_day += GetMonthDay(_year, _month);
_month--;
if (_month < 0)
{
_year--;
_month = 12;
}
}
return *this;
}
bool Date::operator>(const Date& d)const
{
if (_year > d._year)
return true;
else if (_year == d._year && _month > d._month)
return true;
else if (_year == d._year && _month == d._month && _day > d._day)
return true;
else
return false;
}
bool Date::operator==(const Date& d)const
{
return _year==d._year&&_month==d._month&&_day==d._day;
}
bool Date::operator < (const Date& d)const
{
if (*this <= d && *this != d)
return true;
else
return false;
}
bool Date::operator <= (const Date& d)const
{
return !(*this>d);
}
bool Date::operator != (const Date& d)const
{
return !(*this==d);
}
int Date::operator-(const Date& d)
{
int ret = 1;
int count = 0;
if (*this < d)
ret = -1;
Date max = (*this) > d ? *this : d;
Date min = (*this) < d ? *this : d;
while (min<max)
{
min++;
count++;
}
return ret * count;
}
这里许多成员的函数的实现是借助其他成员函数来实现的,这样函数的复用性就会大大提高,便于以后代码的修改。
const成员函数
const修饰类的成员函数
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
例如上面的 日期打印函数:
void print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
为了防止在print成员函数中对类成员变量进行修改,所以这里我们最好把print函数写成const成员函数
void print() const
{
cout << _year << "/" << _month << "/" << _day << endl;
}
注意:
- const对象是不能调用非const成员函数,防止通过非const成员函数对const进行修改
- 非const函数是可以调用const对象的
- const成员函数是不能调用非const成员函数
- 非const函数是可以调用const成员函数的
- const只能修饰非静态成员函数
取地址及const取地址操作符重载
这两个函数写不写都无所谓,因为编译器会自动生成
class A
{
public:
A* operator&()
{
return this;
}
const A* operator&()const
{
return this;
}
};
static成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定在类外初始化。
class A
{
public:
A(int x=1)
:_a1(x)
{
count++;
}
static int Get_count()
{
return count;
}
private:
int _a1;
static int count;
};
int A::count = 0;
int main()
{
A a1(1);
A a2(2);
A a3[3];
cout << A::Get_count() << endl;;
return 0;
}
特性:
- 静态成员为所有类对象所共享,不属于某个具体实例,属于整个类
- 静态成员变量必须在类外定义,定义时不添加static关键字
- 类静态成员即可用
类名:: 静态成员 或者 对象.静态成员 来访问 - 静态成员函数没有隐藏this指针,不能访问任何非静态成员
- 静态成员和类的普通成员一样,也有public,protected,private三种访问级别,也可以具有返回值
- static成员是在静态区上开辟空间,所以计算sizeof(类)的时候是不计算static成员的
友元
友元函数
友元函数可以直接访问类的私有成员,他是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时要加friend 关键字
特性:
- 友元函数可访问类的私有成员和保护成员,但不是类的成员函数
- 友元函数不能用const修饰(只有含有this指针的函数才能被const修饰)
- 友元函数可以在类定义的任何地方声明,不受访问限定符限制(public,protected,private)
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
友元类
概念: 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员
注意:
- 友元关系是单向的,不具有交换性(例如:类A是类B的友元,那么B就可以访问类B里的所有成员,但是反过来却不行。)
- 友元关系不能传递(A是B的友元,B是C的友元,则不能说明A是C的友元)
内部类
概念: 如果一个类定义在另一个类的内部,就叫做内部类。
特性:
- 内部类是一个独立的类,他不属于外部类,更不能通过外部类的对象去调用内部类。
- 外部类对内部类没有访问权限的 !
- 内部类可以直接访问外部类中的任意成员,不需要外部类的对象/类名
sizeof(外部类)=外部类 内部类不参与外部类大小计算- 内部类和友元类没啥区别,只不过收到外部类的类域的限制(不同的访问限定符决定不同的访问权限)
class A
{
public:
A(int x = 1)
:_a1(x)
{
}
class B
{
public:
B(int b=1)
{
_b1 = b;
}
private:
int _b1;
};
private:
int _a1;
};
int main()
{
A a1;
A::B b1;
}
关于类的两个问题
编译器优化拷贝构造函数
class A
{
public:
A(const A& d)
{
cout << "拷贝" << endl;
}
A& operator = (const A& a)
{
cout << "=" << endl;
_a = a._a;
return *this;
}
private:
int _a;
};
A fun(A a)
{
return a;
}
void fun1(A a)
{
}
例一:
int main()
{
A a1;
fun1(a1);
}
众所周知:函数传值是传的实参的一份拷贝,所以这里会调用一次拷贝构造函数
例二:
int main()
{
A a1;
A a2 = fun(a1);
}
前面传值给函数函数 有一次拷贝构造 这里是把函数的返回值拷贝构造a2(注意这里不是赋值重载!!),函数传返回值我们知道是先传给一个构造的中间变量,再由中间变量传给主函数的值。所以这里单纯传返回值就应该有两次拷贝构造 所以这里总共应该有三次可拷贝构造!!!! 但实际上:只有两次拷贝构造 这里对传返回值做了简化:
构造、析构函数的调用先后问题
class A
{
public:
A(int x=1)
:_a1(x)
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a1;
};
A a;
int main()
{
A b;
A c;
static A d;
}
这里构造函数的调用顺序是:a b c d 析构函数的调用顺序是:c b d a
注意:
- 全局变量永远是最先构造,在进入main函数之前就会完成构造
- 函数内的静态变量由于存在静态区,所以出函数不会被销毁,顾析构函数调用顺序在形参的后面
- 静态变量 比全局变量先析构
|