1.运算符重载
1.1 普通运算符重载
在类内重写operator+函数,实现加号运算符的重载,下面给出了两种调用方式,注意加号前为调用者,加号后为参数,第三行代码的完整写法实际上是第四行
1 2 3 4
| Time Time::operator+(int minutes)const; Time time; Time time2 = time+50; Time time3 = time.operator+(50);
|
1.2 运用友元实现运算符重载
上述运算符重载存在一个问题,50 + time 是无效的,因为50没有对应的加法运算符重载,我们可以使用友元解决
- 虽然 operator+() 函数在类内声明,但它并不是成员函数
- 虽然 operator+() 不是成员函数,但它与成员函数访问权限相同
1 2 3 4 5 6 7
| friend Time operator+(int minutes, const Time& t);
Time operator+(int minutes, const Time& t);
Time time2 = 50 + time; Time time3 = operator+(50, time);
|
1.3 其他运算符重载
同样使用友元实现左移运算符的重载
1 2 3 4 5 6 7 8 9 10
| friend std::ostream& operator<<(std::ostream& os, const Time& time);
std::ostream& operator<<(std::ostream& os, const Time& time){ cout << time.hours << " hours, " << time.minutes << " minutes" << endl; return os; }
Time time; cout<<time;
|
1.4 示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| #include "time.h" #include <iostream> int main() { Time time1, time2; time1.setHours(1); time1.setMinutes(30); time2.setHours(5); time2.setMinutes(55); Time time3 = time1 + time2; Time time4 = time1.operator+(time2); Time time5 = time1 + 50; Time time6 = 50 + time1; Time time7 = operator+(50, time1); std::cout << time1 << time2 << time3 << time4 << time5 << time6 << time7; }
#pragma once #include <ostream> class Time { private: int hours; int minutes; public: void setHours(int hours) { this->hours = hours; } void setMinutes(int minutes) { this->minutes = minutes; } Time operator+(const Time& t) const; Time operator+(int minutes) const; friend Time operator+(int minutes, const Time& t); friend std::ostream& operator<<(std::ostream& os, const Time& time); };
#include "time.h" #include <iostream> using namespace std;
Time Time::operator+(const Time& t)const { Time sum; sum.minutes = minutes + t.minutes; sum.hours = hours + t.hours + sum.minutes / 60; sum.minutes %= 60; return sum; }
Time Time::operator+(int minutes)const { Time sum; sum.minutes = this->minutes + minutes; sum.hours = hours + sum.minutes / 60; sum.minutes %= 60; return sum; }
std::ostream& operator<<(std::ostream& os, const Time& time) { cout << time.hours << " hours, " << time.minutes << " minutes" << endl; return os; }
Time operator+(int minutes, const Time& t) { Time sum; sum.minutes = t.minutes + minutes; sum.hours = t.hours + sum.minutes / 60; sum.minutes %= 60; return sum; }
|
2. 类和动态内存分配
2.1 静态成员变量初始化
- 不能在类声明中初始化静态成员变量,因为声明只描述如何分配内存,但不分配内存,我们通过这种格式创建对象,从而分配和初始化内存
- 静态变量先于对象创建,而对象创建时才去设置这个变量是不合适的
- 使用const修饰的整数或枚举可以在类的声明中初始化
1 2 3 4 5 6 7 8 9 10
| class student{ public: char* name; static int numofstu; const static int maxname = 5; };
#include "student.h" int student::numofstu = 0;
|
2.2 类的初始化
- 初始化顺序:构造函数中多个项目被初始化的顺序是他们在类中声明的顺序,而不是在初始化列表中的顺序
1 2 3 4 5 6 7 8
| class Student{ private: char* name; double scores; public: Student(double scores, const char* str) : scores(score), name(str){} }
|
- 使用 explicit 防止构造函数的隐式转换,下图给出了如果显示给出 explicit 关键字编译器会报出的错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <iostream> class Square { public: int x; int y; Square(int x = 0, int y = 0) :x(x), y(y) {} };
void printSquare(const Square& s) { std::cout << "Square x: " << s.x << " ," << s.y << std::endl; } int main(){ Square s = 1; printSquare(2); }
|
2.3 特殊成员函数
设计类的过程中如果不正确处理c++自动提供的这些函数可能造成很多问题,必须正确处理
- 默认构造函数
- 默认构造函数不接受任何参数,也不执行任何操作,没有任何数据的初始化等行为
- 默认析构函数
- 默认不做任何处理
- 注意:构造函数可以存在多个,但析构函数只有一个。所以构造函数中 new 或 new[] 必须统一,析构函数只能是new 或 new[]
- 复制构造函数
- 复制构造函数是新建一个对象并将其初始化为同类现有对象时调用的(实际上只要是按值传递都会调用复制构造函数,包括函数传参,返回值)
- 默认复制构造函数会逐个复制非静态成员(浅复制),对于指针可能造成重复回收,或者另一个对象销毁后本对象指向一个空值等各种问题(只要包括new,就必须重写)
- 以下声明都会调用复制构造函数
1 2 3 4
| StringBad ditto(mitto); StringBad metoo = motto; StringBad also = StringBad(motoo) StringBad * pStringBad = new StringBad(motto);
|
- 赋值运算符
- 默认赋值运算符也会存在浅复制的问题,同样应该重写
- 以下声明会调用复制运算符
1 2
| StringBad ditto; ditto = mitto;
|
- 地址运算符
- 一般情况下都没啥问题,特殊情况下可以给取地址运算符返回nullptr避免其他人获取地址
2.4 虚函数
2.4.1 virtual 函数调用
- 如果不使用 virtual 标记,函数不是虚函数,程序只根据引用类型或指针类型调用方法(根据引用者调用方法)
- 如果使用 virtual 标记,函数是虚函数,程序将根据实际指向的对象类型来选择方法(根据实际对象调用方法)
- 基类用virtual标记的函数,派生类不论是否标记都是虚的,但应该标记
1 2 3 4 5 6 7 8 9 10 11 12 13
|
Fruit fruit; Banana banana; Fruit & f1 = fruit; Fruit & f2 = banana; f1.view(); f2.view();
Fruit fruit; Banana banana; Fruit & f1 = fruit; Fruit & f2 = banana; f1.view(); f2.view();
|
- 注意:如果重新定义一个同名的虚函数,不会生成函数的重载版本,子类会隐藏基类的函数版本
- 所以如果基类的声明被重载了,子类想重新定义一个重载版本,必须重新定义所有重载版本,否则其他的重载将被隐藏!
- 特殊情况:如果返回值是基类的引用或指针,则子类可以修改为子类的引用和指针,这不会导致隐藏(这种特性是返回类型协变)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| class Fruit{ public: virtual void show(); } class Banana{ public: virtual void show(int a); } Banana banana; banana.show(5); banana.show();
Fruit b2 = banana; b2.show();
class Fruit{ public: virtual Fruit* build(); } class Banana{ public: virtual Banana* build(); } Banana banana; Fruit b2= banana; banana.show(); b2.show();
|
2.4.2 函数传值
函数传参过程中,引用传递和指针传递都会将对象完整传递过去,而值传递可能只将部分对象传递到函数内
1 2 3 4 5 6 7 8 9
| void fr(Fruit & rf); void fp(Fruit * pf); void fv(Fruit f); int main(){ Banana banana; fr(banana); fp(banana); fv(banana); }
|
2.4.3 虚析构函数
其实这里的原理与普通的 virtual 关键字声明的函数相同,单独哪出来是为了强调和提醒只要做基类的析构函数都应该是虚函数。
如果不用 virtual 关键字声明析构函数,则只会调用指针类型指向的析构函数。例如Fruit* 指向一个Banana,但它只会调用Fruit的析构函数,导致内存泄漏。但如果析构函数是虚函数,将调用相应对象的析构函数,然后自动调用基类的析构函数,这样才可以完整释放该类占用的内存。
2.4.4 复制构造函数
子类的赋值构造函数,在子类有new分配内存的时候也需要重写,但他必须调用基类的复制构造函数来处理基类的数据
1
| hasDMA::hasDMA(const hasDMA & hs) : baseDMA(hs);
|
2.4.5 赋值运算符
子类存在 new 动态分配内存的时候,需要重写赋值运算符,但它作为子类的方法,只能访问子类的数据,但派生类必须处理父类的数据来对父类进行赋值
1 2 3 4 5 6
| hasDMA & hadDMA::operator=(const hasDMA & hs){ if(this == &hs) return *this baseDMA::operator=(hs); }
|
2.4.6 静态联编和动态联编
将源代码中的函数调用解释为指定特定的代码块被称为函数名联编
- 静态联编:普通的函数以及函数重载都可以在编译过程中完成这种联编(static binding、early binding)
- 动态联编:编译器必须生成能够在运行时选择正确的虚函数的代码(dynamic binding、late binding)
- 动态联编通过虚函数表实现,每个类中都有一个隐藏的指针成员vptr,它指向自己这个类的虚函数表
- 在调用函数的时候,不管指针对象是什么,都通过该对象对应的vptr指针来调用它对应的函数
2.5 继承
- 使用私有继承时,只能在派生类的方法中使用基类的方法(第三代基类将不能再直接调用)
- 使用保护继承时,基类的公有成员和保护乘员都将成为派生类的保护乘员(后代仍然可以调用)
- 使用公有继承时,还是public
2.5.1 多重继承
- 多重继承从不同的基类中继承同名方法
- 多重继承从不同的基类中继承同一个类的多个实例
1 2 3 4 5 6
| class Worker{}; class Waiter : public Worker{}; class Singer : public Worker{}; class SingingWaiter : public Waiter, public Singer{}; SingingWaiter ed; Worker * pw = &ed;
|
通常情况下,这种赋值把基类指针设置为派生对象中的基类对象的地址。但是 ed 中包含两个 Worker 对象,有两个地址可以选择,这会产生问题
1 2
| Worker * pw1 = (Waiter *) &ed; Worker * pw2 = (Singer *) &ed;
|
同样如果想调用基类中同名的函数也应该显示的声明
1 2 3 4 5 6
| ed.Waiter::View(); ed.Singer::View();
void SingingWaiter::View(){ Singer::View(); }
|
- 虚基类:虚基类可以让多个基类相同的类,派生出的对象只继承一个基类对象(实际上引入了一种新的规则)
1 2 3 4 5 6 7 8 9 10
| class Singer : virtual public Worker {}; class Waiter : virtual public Worker {}; class SingingWaiter : public Singer, public Waiter {};
SingingWaiter(const Worker & wk, int p=0, int v = Singer::other) : Waiter(wk, p), Singer(wk, v) {}
SingingWaiter(const Worker & wk, int p=0, int v = Singer::other) : Worker(wk), Waiter(wk, p), Singer(wk, v) {}
|
2.6 模板
2.6.1 模板示例和非类型参数
class 指出 T 为类型参数,而后面的 int 指出 n 的类型为 int,这种参数是非类型参数或表达式参数
1 2 3 4 5 6 7 8 9
| template<class T, int n> class Array { private: T data[n]; };
int main(){ Array<int, 100> m; }
|
2.6.2 将模板用作参数
鄙人有些菜,实际使用中一般都是直接传入一个 queue 而不是写下面这种我感觉很复杂的方式
1 2 3 4 5 6 7 8 9 10 11 12
| template <template <class T> class T2> class A{ T2<int> t; }; template <class T> class queue{ T data; }; int main(){ A<queue> a; }
|
2.6.3 友元函数
如果有一个友元函数 View,它的参数是类本身,会存在一个问题,类具体化和友元函数之间没有对应的关系,没有办法直接调用友元函数
1
| friend void View(HasFriend<T> &);
|
- 非模板友元:这种友元函数最简单,直接在声明时写清楚类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| template<class T> class HasFriend { public: T data; friend void View(HasFriend<T>& hf); }; void View(HasFriend<int>& hf) { cout << "模板类的非模板的友元函数,int 类型" << endl; } void View(HasFriend<char>& hf) { cout << "模板类的非模板的友元函数,char 类型" << endl; } int main(){ HasFriend<int> hf; hf.data = 10; View(hf); }
|
- 约束(bound)模板友元
- 声明模板原型 -> 类内声明友元函数 -> 类外实现友元函数
- 通过给定模板类的 T 类型变量,编译器推断对应的友元函数形式
- 模板原型只是一个形式,友元函数实现中”长得接近即可“,参见 View() 的第三个重载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| #include "HasFriendT.hpp"
int main() { HasFriendT<char> hf; hf.data = 'x'; View<char>(); View(hf); View('c', hf); View<char>(hf); }
#pragma once #include<iostream> using namespace std;
template<class T> void View(); template<class T> void View(const T& hf); template<class T1, class T2> void View(T1 t1, T2& t2);
template<class T> class HasFriendT { public: T data; friend void View<T>(); friend void View<HasFriendT<T>>(const HasFriendT<T>& hf); friend void View<T, HasFriendT<T>>(T data, HasFriendT<T>& hf); };
template<class T> void View() { cout << "sizeof(T): " << sizeof(T) << endl; }
template<class T> void View(const HasFriendT<T>& hf) { cout << hf.data << endl; }
template<class T> void View(T data, HasFriendT<T>& hf) { hf.data = data; }
|
- 非约束(unbound)模板友元
- 只需要在类内声明一个友元函数,类外去实现,自由的过分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include "HasFriendT.hpp" int main(){ HasFriendT<char> hf1; HasFriendT<int> hf2; View(hf1, hf2); int a = 1; int b = 2; View(a, b); }
#pragma once #include<iostream> using namespace std; template<class T> class HasFriendT { public: T data; template<class T1, class T2> friend void View(T1& t1, T2& t2); };
template<class T1, class T2> void View(T1& t1, T2& t2) { cout << "友元非约束方式, 这个真是太自由了" << endl; }
|
Author:
mxwu
Permalink:
https://mingxuanwu.com/2023/11/24/202311242229/
License:
Copyright (c) 2023 CC-BY-NC-4.0 LICENSE