目录
前言
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完
成
一、类的认识
1.1 类的引入
C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数
//C++兼容C struct结构体可以继续使用 //同时 struct 也升级为‘类’ struct Stack { //成员函数 void Init() { a = nullptr; top = 0; capacity = 0; } void Push(int x) { //... } //成员变量 int* a; int top; int capacity; }; //C语言 void StackInit(struct Stack* ps) { //... } void StackPush(struct Stack* ps, int x) { //... } int main() { struct Stack st1; // C/C++ StackInit(&st1); StackPush(&st1, 1); StackPush(&st1, 2); StackPush(&st1, 3); Stack st2; // C++ st2.Init(); st2.Push(1); st2.Push(2); st2.Push(3); return 0; }
1.2 类的定义
上面结构体的定义,在C++中更喜欢用class来代替
class className { // 类体:由成员函数和成员变量组成 }; class 为定义类的关键字, ClassName 为类的名字, {} 中为类的主体,注意类定义结束时后面分号不能省略。 类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者 成员函数。
类的两种定义方式
1.声明和定义全部放在类体中,需注意:成员函数如果在类中定义, 编译器可能会将其当成内联函数处理。 class Stack { //成员函数 void Init() { a = nullptr; top = 0; capacity = 0; } void Push(int x) { if (top == capacity) { size_t newcapacity = capacity == 0 ? 4 : capacity *= 2; a = (int*)realloc(a, sizeof(int) * newcapacity); capacity = newcapacity; } a[top++] = x; } int Top() { return a[top - 1]; } //成员变量 int* a; int top; int capacity; }; 2.声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名:: //func.h class Stack { //成员函数 void Init(); void Push(int x); int Top(); //成员变量 int* a; int top; int capacity; }; 类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。 //func.cpp #include "func.h" void Stack::Init() { a = nullptr; top = 0; capacity = 0; } void Stack::Push(int x) { if (top == capacity) { size_t newcapacity = capacity == 0 ? 4 : capacity *= 2; a = (int*)realloc(a, sizeof(int) * newcapacity); capacity = newcapacity; } a[top++] = x; } int Stack::Top() { return a[top - 1]; }
类的成员变量命名规则建议
class Date { //成员函数 void Init(int year, int month, int day) { //如果成员变量名为year会发生什么? // year = year; //你还能够辨认出上面的year哪个是成员变量、哪个是成员函数吗? _year = year; _month = month; _day = day; } void Print() { cout << _year << "/" << _month << "/" << _day <<endl; } //成员变量 int _year; int _month; int _day; }; int main() { Date d; d.Init(2023, 7, 21); d.Print(); return 0; } //因此我们在给成员变量命名时最好加上一些修饰 //例如:_year year_ mYear
1.3 类的访问限定符
类的访问限定符有public、protected和private
1. public修饰的成员在类外可以直接被访问
2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止如果后面没有访问限定符,作用域就到 } 即类结束。
4. class的默认访问权限为private,struct为public(因为struct要兼容C)注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
类的访问限定符有什么作用呢?面向对象的三大特性:封装、继承、多态。封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。封装本质上是一种管理,让用户更方便使用类。在类和对象阶段,主要是研究类的封装特性。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,选择性的将其接口提供给外部的用户使用
1.4 类的实例化
用类类型创建对象的过程,称为类的实例化
1. 类是对对象进行描述的,好比设计图纸一样的存在,仅仅确定了类有哪些成员,因此定义出一个类并没有分配实际的内存空间来存储它。比如:入学时填写的学生信息表,表格就可以看成是一个类,来描述具体学生信息
2. 一个类可以实例化出多个对象,实例化出的对象才占用实际的物理空间,存储类成员量。 类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是实际上房子还没有建成
1.5 类对象的大小及存储方式
一个类的大小,实际就是该类中“成员变量”的大小之和,要注意内存对齐(与结构体的大小类似)
判断下面类的大小 // 类中既有成员变量,又有成员函数 class A1 { public: void f1() {} private: int _a; }; // 类中仅有成员函数 class A2 { public: void f2() {} }; // 类中什么都没有---空类 class A3 {}; int main() { cout << sizeof(A1) << endl; //4 cout << sizeof(A2) << endl; //1 cout << sizeof(A3) << endl; //1 return 0; } 注意:空类比较特殊,编译器给了空类一个字节来标识这个类的对象,并没有存储数据
类对象的存储方式的探究
下面代码中,d1的成员变量_year与d2的成员变量_year相同吗?d1的成员函数Print()与d2的成员函数Print()相同吗?
int main() { Date d1; d1.Init(2023, 7, 21); d1.Print(); Date d2; d2.Init(2023, 7, 22); d2.Print(); return 0; }
通过反汇编查看汇编代码
我们可以看到, d1的成员函数Print()与d2的成员函数Print()是同一个函数,函数的地址也是一模一样的。实际上类对象的存储方式也是这样的
二、this指针
在类中,成员函数是怎样直接访问到成员变量的呢?成员函数又是怎样区分不同的类对象的成员变量的呢?
实际上C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数(this指针),让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问
在编译器的眼里实际上是这样的:
1. this指针的类型:类的类型* const,即成员函数中,不能给this指针赋值
2. 只能在“成员函数”的内部使用
3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针
4. this指针是“成员函数”第一个隐含的指针形参(this是一个形参,一般存在栈帧里面),一般情况由编译器通过ecx寄存器自动传递,不需要用户传递// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行 class A { public: void Print() { cout << "Print()" << endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); return 0; } // C // p->Print(); // p指针没有被解引用,调用函数Print()不需要去类对象p中寻找函数Print()地址, //因为成员函数的地址并没有保存在类对象中,而是保存在公共的代码区中 // 同理,替换成这条语句也是一样的 (*p).Print(); p指针实际上也没有被解引用 // 2.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行 class A { public: void Print() { cout << _a << endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); return 0; } // B // cout << this->_a << endl; // this 是 nullptr,空指针不能被解引用
三、类的默认成员函数
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数3.1 构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象
1. 函数名与类名相同
2. 无返回值,不需要加上void
3. 对象实例化时编译器自动调用对应的构造函数
4. 构造函数可以重载,我们可以写多个构造函数,提供多种初始化的方式class Date { public: //Date() //初始化 //{ // cout << "Date()" << endl; // _year = 1; // _month = 1; // _day = 1; //} //Date(int year, int month, int day) //初始化 //{ // _year = year; // _month = month; // _day = day; //} //上面的两个函数可以优化合并成下面的函数 Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //我们无需再多实现一个初始化的函数,借助构造函数即可 /*void Init(int year, int month, int day) { _year = year; _month = month; _day = day; }*/ void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; }; int main() { /*Date d1(); //error 编译器可能会认为这是一个函数声明 Date d1(); d1.Print();*/ Date d1; d1.Print(); Date d2(2023, 7, 23); d2.Print(); return 0; }
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。编译器生成默认构造函数的特点:
(1)如果我们自己写了构造函数,编译器就不会自动生成了
(2)内置类型的成员不会处理(一般的编译器都不会处理,但是C++11弥补了这项缺陷,支持在声明处给缺省值)
(3)自定义类型的成员才会处理,会去调用这个成员的默认构造函数
tips:一般情况下都需要我们自己写构造函数,决定初始化方式,除非这个类的成员变量全都是自定义类型 ,我们才可以考虑不写构造函数6. 无参的构造函数和全缺省的构造函数以及我们没写编译器默认生成的构造函数都称为默认构造函数,并且默认构造函数只能有一个(多个并存会产生函数调用不明确)
因为左图只存在一个默认构造函数,而右图存在两个默认构造函数
extra Stack类的初步实现
typedef int DataType; class Stack { public: Stack(size_t n = 4) { if (n == 0) { _array = nullptr; _capacity = 0; _size = 0; } else { _array = (DataType*)malloc(n * sizeof(DataType)); if (_array == nullptr) { perror("malloc fail"); exit(-1); } _capacity = n; _size = 0; } } void Push(DataType data) { CheckCapacity(); _array[_size++] = data; } void Pop() { assert(_size > 0); if (Empty()) return; _size--; } DataType Top() { return _array[_size - 1]; } bool Empty() { return 0 == _size; } int Size() { return _size; } void Destroy() { if (_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; } } private: void CheckCapacity() { if (_size == _capacity) { size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2; DataType* tmp = (DataType*)realloc(_array, newcapacity * sizeof(DataType)); if (tmp == nullptr) { perror("realloc fail"); exit(-1); } _array = tmp; _capacity = newcapacity; } } private: DataType* _array; int _capacity; int _size; }; int main() { Stack st1; st1.Push(1); st1.Push(2); st1.Push(3); st1.Push(4); while (!st1.Empty()) { cout << st1.Top() << " "; st1.Pop(); } cout << endl; st1.Destroy(); //如果已知需要开辟1000个空间 Stack st2(1000); for (size_t i = 0; i < 1000; i++) { st2.Push(i); } while (!st2.Empty()) { cout << st2.Top() << " "; st2.Pop(); } cout << endl; st2.Destroy(); return 0; }
3.2 析构函数
与构造函数功能相反,析构函数的作用是不是完成对对象本身的销毁(对象定义在栈帧中,栈帧结束就自动销毁了),局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
class Stack { //. . . . . . ~Stack() { if (_array) { free(_array); //下面三条语句可以写,也可以不写 //因为只有成员变量_array申请了资源 //_array = nullptr; //_capacity = 0; //_size = 0; } } /*void Destroy() { if (_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; } }*/ //. . . . . . }
析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~
2. 无参数(析构函数不能重载)无返回值类型
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认析构函数(特点与编译器生成默认析构函数的特点相似,只会调用自定义类型成员的默认析构函数)
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数5. 如果类中没有申请资源时,析构函数可以不用写,直接使用编译器生成的默认析构函数即可,比如Date类(因为Date类对象会在栈帧中自动销毁);如果有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类
注意:析构函数保存在栈中,所以后定义的类对象先析构extra Stack类实现括号匹配
bool isValid(const char* s) { Stack st; //不需要自己初始化栈 while (*s) { if (*s == '[' || *s == '(' || *s == '{') { st.Push(*s); ++s; } else { if (st.Empty()) // 不匹配的情况 //不需要自己销毁栈 return false; char top = st.Top(); st.Pop(); if ((*s == ']' && top != '[') // 不匹配的情况 || (*s == ')' && top != '(') || (*s == '}' && top != '{')) return false; ++s; } } //不需要自己销毁栈 return st.Empty(); } 如果使用C语言实现括号匹配,代码将会更加复杂而且容易出错
3.3 拷贝构造函数
我们在对数据进行处理的时候,经常会用到“拷贝”这一操作,C语言中的结构体是允许拷贝的,那么C++中的类可以直接拷贝吗?
我们可以看到,对日期类的拷贝没有产生问题。但是对栈类的拷贝报错了(而不使用析构函数就没有报错了),产生问题的原因在哪里呢?
对于Date类,这样的拷贝方式是没问题的,但是对于Stack类,却是行不通的,我们需要借助拷贝构造函数来实现
拷贝构造函数:只有单个形参,该形参是对同类型对象的引用(一般常用const修饰),在使用已存在的类类型对象创建新对象时由编译器自动调用。拷贝构造函数也是特殊的成员函数,其特征如下:
1. 拷贝构造函数是构造函数的一个重载形式,函数名是类型名称,无返回值
2. 拷贝构造函数的参数只有一个且必须是同类型对象的引用,使用传值方式传参编译器直接报错,因为会引发无穷递归调用
3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝(在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的)
4. 类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数一定要写的,否则就是浅拷贝
(Date类不需要我们写拷贝构造函数,而Stack类就必须写拷贝构造函数)
class Date { public: // ... ... //可以写也可以不写 Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; } private: int _year; int _month; int _day; }; // 注意:下面两种拷贝方式是等价的 // Date d2(d1); // Date d3 = d1; typedef int DataType; class Stack { public: // ... ... //一定要写,否则无法完成拷贝 Stack(Stack& s) { _array = (DataType*)malloc(s._capacity * sizeof(DataType)); if (_array == nullptr) { perror("malloc fail"); exit(-1); } memcpy(_array, s._array, s._size * sizeof(DataType)); _size = s._size; _capacity = s._capacity; } // ... ... private: DataType* _array; int _capacity; int _size; };
自动调用默认成员函数的场景
extra 编译器在拷贝对象时的优化
在传参和传返回值的过程中,一般情况下,编译器可能会做一些优化,减少对象的拷贝,在一些场景下还是非常有意义的——我们可以了解一下,知道即可
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; } void Print() const { cout <<"Print->" << _a << endl; } private: int _a = 0; }; void func(const A& aa = A()) { aa.Print(); }
效果如下:
四、运算符重载
4.1 运算符重载
有时候需要我们比较类对象的大小,但是我们不能直接使用(< > == != >= <=)等运算符直接比较大小,往往需要我们自己实现一个函数来比较类对象的大小,然而我们很容易误判函数得出的结果到底表示谁大谁小
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)注意,内置类型比较大小编译器会自行处理,自定义类型比较大小才会调用运算符重载
如果我们直接以函数的形式定义,代码的可读性较差。使用运算符重载,我们不仅可以以函数调用的形式比较大小,还可以直接使用重载后的运算符比较自定义类型的大小。但是如果类的成员变量是私有的,那么上述代码也无法正常运行
我们可以考虑多种解决问题的方法,其中一种就是把operator<变为成员函数
使用运算符重载还有一些需要注意的问题
1. 不能通过连接其他符号来创建新的操作符,比如 operator@
2. 重载操作符必须有一个类类型的参数
3. 用于内置类型的运算符,其含义不能被改变,例如:内置的整型+,不能改变其含义
4. 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
5. .* 、 :: 、 sizeof 、 ?: 、 . 、 注意以上5个运算符不能重载6. 不能改变操作符的操作数,比如 '+' 的操作数是两个。一个操作符有几个操作数,那么重载的时候就有几个参数
4.2 赋值运算符重载
1. 参数类型:const Date&,传递引用可以提高传参效率
2. 返回值类型:Date&,返回引用可以提高返回的效率,返回*this目的是为了支持连续赋值
3. 检测是否自己给自己赋值,减少不必要的操作4. 赋值运算符重载也是类的默认成员函数,赋值运算符只能重载成类的成员函数不能重载成全局函数(赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数)
5. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝(与拷贝构造类似,内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值)日期类的赋值运算符重载 class Date { // ... Date& operator=(const Date& d) { if (this != &d) // 避免 d1 = d1; 进行无效操作 { _year = d._year; _month = d._month; _day = d._day; } // 支持 d1 = d2 = d3; return *this; } // ... }
4.3 前置++ 与 后置++重载
class Date { // ... // 运算符重载 Date& operator+=(int day) { if (day < 0) { return *this -= (-day); } _day += day; while (_day > GetMonthDay(_year, _month)) { // day满month+1 _day -= GetMonthDay(_year, _month); ++_month; // month满year+1 if (_month == 13) { ++_year; _month = 1; } } return *this; } // 前置++ Date& Date::operator++() { *this += 1; return *this; } // 后置++ Date Date::operator++(int) { Date tmp(*this); *this += 1; return tmp; } // ... } // 前置++和后置++都是一元运算符,为了让前置++与后置++构成函数重载 // C++规定:后置++重载时多增加一个int类型的参数 // 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存 //一份,然后给this+1,而temp是临时对象,因此只能以值的方式返回,不能返回引用
extra Date类的实现(比较详细)
//Date.h #pragma once #include <iostream> using namespace std; class Date { public: // 获取月份的天数 int GetMonthDay(int year, int month); // 全缺省的构造函数 Date(int year = 2000, int month = 1, int day = 1); // 打印输出日期 void Print(); //Q:为什么拷贝构造必须传引用,而赋值运算符重载不会无限循环呢? //A:(两边的对象已经存在)赋值 != 传参; // (一个已存在的对象去初始化另一个对象)拷贝 == 传参 // 拷贝构造 Date(const Date& d); // 赋值运算符重载 this = *d Date& operator=(const Date& d); // 析构函数 ~Date(); // 日期+=天数 Date& operator+=(int day); // 日期+天数 Date operator+(int day); // 日期-=天数 Date& operator-=(int day); // 日期-天数 Date operator-(int day); // 前置++ ++d -> d1.operator++ Date& operator++(); // 后置++ d++ -> d1.operator++(int) // 增加一个int参数,与前置++构成函数重载进行区分 Date operator++(int); // 后置-- Date operator--(int); // 前置-- Date& operator--(); // < 运算符重载 bool operator<(const Date& d); // == 运算符重载 bool operator==(const Date& d); // <= 运算符重载 bool operator<=(const Date& d); // > 运算符重载 bool operator>(const Date& d); // >= 运算符重载 bool operator>=(const Date& d); // != 运算符重载 bool operator!=(const Date& d); // 日期-日期 返回天数 int operator-(const Date& d); private: int _year; int _month; int _day; }; //Date.cpp #include "Date.h" // 获取月份的天数 int Date::GetMonthDay(int year, int month) { static int monthArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 }; if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)) return 29; return monthArray[month]; } // 打印输出日期 void Date::Print() { cout << _year << "/" << _month << "/" << _day << endl; } // 全缺省的构造函数(缺省参数只能在声明处给出) Date::Date(int year, int month, int day) { _year = year; _month = month; _day = day; // if (month < 1 || month > 12 || day < 1 || day > GetMonthDay(year, month)) cout << "非法日期" << endl; } // 拷贝构造 Date::Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; } // 赋值运算符重载 Date& Date::operator=(const Date& d) { if (this != &d) // 避免 d1 = d1; { _year = d._year; _month = d._month; _day = d._day; } // 支持 d1 = d2 = d3; return *this; } // 析构函数 Date::~Date() { _year = 0; _month = 0; _day = 0; } // +=复用+ //Date Date::operator+(int day) //{ // Date tmp = *this; // tmp._day += day; // while (tmp._day > GetMonthDay(tmp._year, tmp._month)) // { // // day满month+1 // tmp._day -= GetMonthDay(tmp._year, tmp._month); // ++tmp._month; // // month满year+1 // if (tmp._month == 13) // { // ++tmp._year; // tmp._month = 1; // } // } // return tmp; //} //Date& Date::operator+=(int day) //{ // *this = *this + day; // return *this; //} // +复用+= //+复用+=(0次拷贝) +=复用+(2次拷贝、1次赋值) // 日期+=天数 Date& Date::operator+=(int day) { if (day < 0) { return *this -= (-day); } _day += day; while (_day > GetMonthDay(_year, _month)) { // day满month+1 _day -= GetMonthDay(_year, _month); ++_month; // month满year+1 if (_month == 13) { ++_year; _month = 1; } } return *this; } // 日期+天数 Date Date::operator+(int day) { Date tmp(*this); tmp += day; return tmp; //出了作用域tmp就不在了,不能用引用返回 } // 日期-=天数 Date& Date::operator-=(int day) { if (day < 0) { return *this += (-day); } _day -= day; while (_day <= 0) { _month--; if (_month <= 0) { _month = 12; _year--; } _day += GetMonthDay(_year, _month); } return *this; } // 日期-天数 Date Date::operator-(int day) { Date tmp(*this); *tmp -= day; return tmp; } // 前置++ Date& Date::operator++() { *this += 1; return *this; } // 后置++ Date Date::operator++(int) { Date tmp(*this); *this += 1; return tmp; } // 后置-- Date Date::operator--(int) { Date tmp(*this); *this -= 1; return tmp; } // 前置-- Date& Date::operator--() { *this -= 1; return *this; } // < 运算符重载 bool Date::operator<(const Date& d) { 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) { return _year == d._year && _month == d._month && _day == d._day; } // <= 运算符重载 bool Date::operator<=(const Date& d) { return *this < d || *this == d; } // > 运算符重载 bool Date::operator>(const Date& d) { return !(*this <= d); } // >= 运算符重载 bool Date::operator>=(const Date& d) { return !(*this < d); } // != 运算符重载 bool Date::operator!=(const Date& d) { return !(*this == d); } // 日期-日期 返回天数 int Date::operator-(const Date& d) { cout << "日期-日期 返回天数" << endl; Date max = (*this > d) ? *this : d; Date min = (*this > d) ? d : *this; int flag = (*this > d); int day = 0; while (min != max) { ++day; ++min; } return day * flag; }
五、const与static
5.1 const
我们把const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际上修饰的是该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改
1. 什么时候加上const修饰成员函数?
只读函数(内部不涉及成员修改的)都可以加上const(加在函数的末尾,定义处与声明处都要加上,加上const的函数权限更小,更容易被调用),但是不能随便给成员函数加const
当出现有时候需要修改类对象,有时候禁止修改类对象的情况,可以考虑写两个成员函数构成函数重载
思考下面几个问题:
1. const对象可以调用非const成员函数吗? X 权限的放大
2. 非const对象可以调用const成员函数吗? √ 权限的缩小
3. const成员函数内可以调用其它的非const成员函数吗? X 权限的放大
4. 非const成员函数内可以调用其它的const成员函数吗? √ 权限的缩小
2. (取地址/const取地址)操作符重载
5.2 static
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
1. 统计累计创建的对象数目,统计正在使用的对象数目
// 统计累计创建的对象数目 // 统计正在使用的对象数目 class A { public: A() { ++n, ++m; } A(const A& t) { ++n, ++m; } ~A() { --m; } // 静态成员函数--没有this指针 static void Print() { // e = 1; //error // 静态成员函数不能访问非静态成员(没有this指针) cout << n << " " << m << endl; } private: // 声明 静态成员变量属于所有A类对象(整个类) // 累计创建的对象数目 static int n; // 正在使用的对象数目 static int m; // sizeof(A) == 4 static定义在全局,不占用类的空间 // static int n = 0; //error,n不通过初始化列表定义,而是在类的外部定义 int e; }; // 定义 int A::n = 0; int A::m = 0;
效果如下:
1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制
2. 求1+2+3+...+n,要求:不能使用乘除法和位运算、for、while、if、else、switch、case等关键字及条件判断语句
class Sum { public: Sum() { _ret += _count; _count++; } static int GetRet() { return _ret; } private: static int _count; static int _ret; }; int Sum::_count = 1; int Sum::_ret = 0; class Solution { public: int Sum_Solution(int n) { Sum a[n]; return Sum::GetRet(); } };
1. 静态成员函数可以调用非静态成员函数吗? X
2. 非静态成员函数可以调用类的静态成员函数吗? √
六、深入构造函数
6.1 初始化列表
typedef int DataType; class Stack { public: // ... //Stack(size_t) //不是默认构造函数 Stack(size_t n = 4) //默认构造函数 { // ... } // ... private: // ... }; //对于自定义类型的成员变量,它会调用自己的默认构造函数 //所有我们不必给出MyQueue的构造函数 //但是,如果Stack不提供默认构造函数呢? //我们也无法在MyQueue给出自定义类型的成员变量的构造函数 class MyQueue { public: private: Stack _s1; Stack _s2; };
要想解决上面的问题,我们需要借助初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟上一个放在括号中的初始值或表达式
class Date { public: Date(int year, int month, int day) // 初始化列表是类的成员变量定义的地方 : _year(2000) , _month(month) , _day(day) { // 支持混和使用, // 函数体中不是变量初始化的地方,是赋初值的地方 _year = year; // _year最终的初始值由形参year决定 _day = day; } private: int _year; // 类的成员变量声明的地方,不开辟空间 int _month; int _day; };
1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2. 类中包含的以下成员,必须放在初始化列表位置进行初始化:
引用成员变量、const成员变量、自定义类型成员(且该类没有默认构造函数时)
3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化
4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
6.2 explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用
上述代码可读性不是很好,用explicit修饰构造函数,可以禁止构造函数的隐式类型转换
6.3 匿名对象
七、友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用
7.1 友元函数
如果尝试去重载operator<< operator>>,会发现没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针争抢第一个参数的位置。this指针默认是第一个参数也就是左操作数,但是实际中使用cout需要的也是第一个形参对象,所以要将operator<<重载成
全局函数,但又会导致类外没办法访问成员。
我们有几种解决的方案,公开成员变量、创建成员函数Get_成员变量、友元,接下来我们演示一下友元怎样解决问题的友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字
1. 友元函数可访问类的私有和保护成员,但不是类的成员函数
2. 友元函数不能用const修饰
3. 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
4. 一个函数可以是多个类的友元函数
5. 友元函数的调用与普通函数的调用原理相同7.2 友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员
lass Time { friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量 public: Time(int hour = 0, int minute = 0, int second = 0) : _hour(hour) , _minute(minute) , _second(second) {} private: int _hour; int _minute; int _second; }; class Date { public: Date(int year = 1900, int month = 1, int day = 1) : _year(year) , _month(month) , _day(day) {} void SetTimeOfDate(int hour, int minute, int second) { //可以直接访问时间类私有的成员变量 _t._hour = hour; _t._minute = minute; _t._second = second; } private: int _year; int _month; int _day; Time _t; };
但是,需要注意一下几点
1. 友元关系是单向的,不具有交换性(你把别人当朋友,别人不一定也把你当朋友)
例如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接
访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行
2. 友元关系不能传递(你的朋友的朋友,不是你的朋友)
如果C是B的友元, B是A的友元,则不能说明C时A的友元。
3. 友元关系不能继承(别人替代了你的岗位,也不能继承你的人际关系)
7.3 内部类
如果一个类定义在另一个类的内部,这个在内部的类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限
注意:内部类就是外部类的友元类,参见友元类的定义。内部类可以通过外部类的对象参数来访问外部类中的所有成员,但是外部类不是内部类的友元1. 内部类可以定义在外部类的public、protected、private都是可以的
2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名(但是外部类不能直接访问内部类)
3. sizeof(外部类)=外部类,和内部类没有任何关系如果某个类仅仅是为了另外一个类服务的,我们不希望外部看到它(使用它),可以采用内部类的形式进行优化
求1+2+3+...+n,要求:不能使用乘除法和位运算、 for、while、if、else、switch、case等关键字及条件判断语句 class Solution { class Sum { public: Sum() { _ret += _count; _count++; } }; public: int Sum_Solution(int n) { Sum a[n]; return _ret; } private: static int _count; static int _ret; }; int Solution::_count = 1; int Solution::_ret = 0;
感谢各位读者的阅览,欢迎广大网友提出建议、批评指正~