面向对象高级开发
Header头文件的防卫式声明
//complex.h
#ifndef __COMPLEX__
#define __COMPLEX__
//含义:如果程序没有定义过,那么定义出来,走主体内容;如果程序定义过,第二次include时,那么就不走,直接返回,不会有重复include的动作
/*
主体内容
*/
#endif
inline 函数
class complex{
public:
complex();
double real() const {return re; }//如果函数在类内声明并且定义完成,那么这个函数就是个inline函数
private:
double re,im;
};
inline double
real(const complex& x){
return x.real();
}//inline的执行速度会快一点!
为了减少时间开销,如果在类体中定义的成员函数中不包括循环等控制结构,C++系统会自动将它们作为内置(inline)函数来处理。
C++要求对一般的内置函数要用关键字inline声明,但对类内定义的成员函数,可以省略inline,因为这些成员函数已被隐含地指定为内置函数。
如果成员函数不在类体内定义,而在类体外定义,系统并不把它默认为内置(inline)函数,调用这些成员函数的过程和调用一般函数的过程是相同的。如果想将这些成员函数指定为内置函数,应当用inline作显式声明。
如果函数太复杂,编译器没有办法把他看成 inline 函数!! 所以我们在类内实现只是为了建议编译器将其看成 inline 函数来提高效率,但是实际上是不是 inline 函数要看编译器,我们也不知道!
单例设计模式(构造函数在private部分)
例子:
//头文件 Stu.h
#ifndef __STU__
#define __STU__
#include <string>
using namespace std;
class Stu
{
public:
static Stu &getInstance();//单例设计模式 在主程序当中只能使用一份这个类的数据 所有共享 所以静态变量放在堆区
void setup() {}
private:
Stu();
Stu(int id, string name);
int _ID;
string _name;
};
Stu &Stu::getInstance()
{
static Stu stu;
return stu;
}
#endif
//主程序 main.cpp
#include <iostream>
#include "Stu.h"
int main()
{
// 单例设计模式
// 构造函数在private里面 整个类是放在static堆区的 所有用户只用一份这个类的数据
auto stu = Stu::getInstance(); // 这样就创造出来了一个类 并且是静态变量!!!程序共享这一份
return 0;
}
const 常量成员函数
//complex类
class complex
{
public:
complex(double r = 0, double i = 0) : re(r), im(i) {}
double real() const { return re; }
double imag() const { return im; }//成员函数 不改变类成员属性的值 建议加上const修饰,换句话说就是拿数据
private:
double re, im;
};
成员函数不改变类成员属性的值建议加上const修饰,换句话说就是拿数据.例如上面的real()和imag()都不改变成员属性的值,所以加上了const修饰。
在上面的例子当中,如果那两个成员函数不加上const,会出现什么情况呢?
//主函数
const complex c(1,2);//使用者定义这个类是不可以改变的
std::cout<<c.real();//打印real()
这是未加const的成员函数
double real() { return re; }
double imag() { return im; }
打印real()会出问题,因为使用者不想要改变c里面元素的值,但是这个成员函数在访问的时候不加const,编译器认为有可能会改变成员函数的值,这两者是相互矛盾的,所以会报错,所以需要加上const来保证是不会改变值的。
引用(指针常量 指针指向不可修改)
引用的本质: 是一个指针常量
为什么选择引用: 传递非常快,并且可以解决形参修改不改变实参的问题
int a=10;
//自动转换为 int* const ref = &a; 指针常量是指针指向不可改,也说明为什么引用不可更改
int& ref = a;
ref = 20; //内部发现ref是引用,自动帮我们转换为: *ref = 20;
引用是不可修改的,因为引用本质是指针常量,该指针的值是不可修改的,也就是指向的地址区域(a)是不可以变动的,但是解引用修改指向区域的值是完全没有问题的。
所以考虑到这两个问题,在实际操作过程中尽量传入引用。
friend 友元
//complex类
class complex
{
public:
complex(double r = 0, double i = 0) : re(r), im(i) {}
double real() const { return re; }
double imag() const { return im; }//成员函数 不改变类成员属性的值 建议加上const修饰,换句话说就是拿数据
private:
double re, im;
friend complex &__doapl(complex*, const complex& );//第二个参数传入另一个类对象的引用
//friend友元表示另一个类对象可以访问本类当中的私有成员属性
};
inline complex &
__doapl(complex *ths, const complex &r)
{
ths->re += r.re;//这里就可以直接访问本类的私有成员属性
ths->im += r.im;
return *ths;
}
重点:
相同class的各个objects互为friends 友元
还是上面的例子
//类内
int func(const complex& param){ return param.re + param.im;}
//这里为什么可以直接访问私有成员属性
//主函数
c2.func(c1);
两种理解:
1.相同class的各个objects互为友元,所以可以访问私有属性
2.私有成员属性可以类内访问,类外不可以访问,需要访问需要成员函数接口
return by *
return by reference
传送着无需知道接收者是以reference接受
inline complex&
complex::operator += (const complex &r){
return __doapl(this,r);
}
实现 += 号的重载(复数)
为什么要返回 & :因为不能创建新对象,我们的目的是修改原对象!!!
这样做可以实现连加操作 c1 += c2 + =c3
关于运算符重载返回reference还有一个例子
#include <iostream>
using namespace std;
#include <string>
#include <ostream>
class Stu
{
public:
// 类内的函数会默认为内置inline函数
Stu();
Stu(int id, string name) : _ID(id), _name(name) {}
int getID() { return this->_ID; }
string getName() { return this->_name; }
private:
int _ID;
string _name;
};
//这种特殊的操作符重载只能写在全局,因为写在类里面无法达到 cout<<p 的效果
inline ostream & //返回ostream标准流的引用
//关于返回值 需要考虑连传的话需要返回引用!!!
operator<<(ostream &os, Stu &s)
{
os << s.getID() << ' ' << s.getName() << endl;
return cout;
}
int main()
{
Stu s(1, "张三");
cout << s;//这样可以实现连 <<
return 0;
}
return by value:
什么时候只能return by value?我们应该优先考虑return by reference,但是在这个函数返回值的时候需要创建一个新的对象的时候只能return by value
inline complex
operator+(const complex &x,const complex &y){
return complex(real(x)+real(y),imag(x)+imag(y));
}
c2=c1+c2;//这行代码的意思是c1+c2创建出来一个新的对象赋值给c2!!!
class with pointer members 带有指针的类
比较经典的类就是string字符串类,必须有拷贝构造 copy ctor和拷贝赋值 copy op=
class String
{
public:
String(const char *cstr = 0);
// 只要类带指针,一定要重写以下两个函数!!!
// 不重写的话编译器默认的构造函数是浅拷贝 两个指针指向同一块内存对象!!!
// 当然这里的拷贝指的是深拷贝
String(const String &str); // 拷贝构造
String &operator=(const String &str); // 拷贝赋值
~String();
char *get_c_str() const { return this->_data; }
private:
char *_data;
};
当然这里的拷贝指的是深拷贝!!!
Big Three:拷贝构造,拷贝赋值,析构函数!!!
拷贝赋值:检测自我赋值
inline String &String::operator=(const String &str)
{
// 深拷贝赋值 先把自身杀掉 然后重新创建
// 检测自我赋值
if (this == &str)
return *this;
delete[] this->_data;
// 深拷贝
this->_data = new char[strlen(str) + 1];
strcpy(this->_data, str._data);
return *this;
}
这里为什么要检测自我赋值:因为不检测自我赋值的话,如果使用者调用自我赋值的时候,第一步就会把唯一的自身这个_data杀掉,后续就没有办法进行了,会出现安全隐患!!!这也是为了安全和严谨性考虑的
一些对象的生命期
class complex{ };
complex c3(1,2);
int main(){
complex c1(1,2);
static complex c2(1,2);
complex *c4=new complex(1,2);
delete c4;
return 0;
}
C++程序在执行时,将内存大方向划分为4个区域
代码区:存放函数体的二进制代码,由操作系统进行管理。
全局区:存放全局变量和静态变量以及常量。
栈区:由编译器自动分配释放,存放函数的参数值,局部变量等。
堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
c1 :存放在栈当中,当作用域(这里是main函数)结束的时候就会被自动清理
c2 :静态变量,存放于全局静态区,当整个程序结束之后才会被释放
c3: 全局变量,存放于全局区,当整个程序结束之后才会被释放
c4: new出来的,动态分配内存,存放于堆区,注意new了之后记得在作用域结束之前将其delete掉
否则会出现内存泄露的问题,当作用域结束之后c4指针会被释放掉,但是他所指向的内存没有被释放!!!一般写析构函数解决这个问题
new和delete
new: 先分配内存空间,再调用构造函数
delete: 先调用析构函数,再释放内存空间
array new 一定要搭配 array delete!!!
写了 [] 的话编译器才会知道你不仅要删除p这个类对象指针,还要把这个p对象指针指向的数组元素给全部删除掉,因为 String* p既可以表示单个的类对象指针也可以表示类对象数组的首元素地址指针!!! 所以必须要要写 [] ,否则只会删除掉p[0]所对应的元素!!!
static 静态
静态变量和静态函数很特殊,具体看下面代码:
静态函数由于没有this指针,所以只能处理静态变量!!!
#include <iostream>
using namespace std;
class Account
{
public:
static double _rate; // 静态成员变量一个程序只有一份,类内声明,类外初始化
static void set_rate(const double &rate); // 静态成员函数,调用时可以声明类对象,可以调用作用域直接访问,类外实现,类外实现的时候不用加关键字static,但是要加作用域
};
double Account::_rate = 8.0;
void Account::set_rate(const double &rate)
{
Account::_rate = rate;
}
int main()
{
cout << Account::_rate << endl;
Account::set_rate(7.0);
cout << Account::_rate << endl;
return 0;
}
进一步补充:把构造函数放在 private 里面,单例设计模式
class Stu
{
public:
static Stu &getInstance() { return s; }
void setup()
{
; // 一系列的接口操作
}
private:
Stu();
Stu(int id, string name);
int _ID;
string _name;
static Stu s;
};
这里的就是把构造函数放在 private 当中,对外界的接口就是这个 getInstance(),这个函数返回静态变量 s ,只有一份,通过 setup() 接口进行对这个类内成员的访问和修改!!!
Stu::getInstance().setup();
优化的写法:
由于这里在构建类的时候就引入了静态成员变量,在没有调用的时候可能会导致资源浪费,所以我们将这个静态成员变量在静态函数中创建就好了,在调用的时候创建,然后生命周期一直持续到程序结束
static Stu& getInstance(){
static Stu s;
return s;
}
模板
类模板:
用的时候必须要明确指出里面参数的类型
template <typename T>
class complex{
public:
complex();
private:
T real;
T imag;
};
int main()
{
complex<int>c1();
complex<double>c2();
}
函数模板:
用的时候不需要指明函数参数的类型,因为编译器会进行实参的推导
class stone
{
public:
stone();
stone(int w, int h, int weight) : _w(w), _h(h), _weight(weight) {}
bool operator<=(const stone &sto) { return this->_weight <= sto._weight; }
private:
int _w, _h, _weight;
};
// 全局函数 比大小
template <typename T>
inline T &
min(T &a, T &b)
{
return a <= b ? a : b ?
}
int main()
{
int a = 1, b = 2, c;
stone a1(1, 2, 3), a2(4, 5, 2), a3;
c = min(a, b);
a3 = min(a1, a2);
}
命名空间 namespace
将自己写的东西封装在一个命名空间当中,可以防止与其他人名称一样功能不同的问题
namespace std{
;
}
复合 composition
简单来理解就是 一个类包含另一个类对象,本类可以调用另一个类的底层函数
//Adapter
template <class T>
class queue
{
protected:
deque<T> c; //底层容器
public:
//以下的操作全都是由c的底层函数执行
bool empty()const{return c.empty();}
};
这里其实包含另一种设计模式: Adapter
deque是双端队列,queue是单端队列,显然deque的功能要比queue功能强大,他完全可以适配(Adapter)queue的功能,所以可以采用复合的方式,queue的成员函数调用deque中的部分成员函数来实现自己的功能!!!
用图可以这样表示:
复合下的构造和析构
构造: 构造由内而外 注意先调用的是内部的默认构造,编译器指定的,也符合我们的预期!
析构: 析构由外而内
这些是编译器帮我安排好的,上面是我们希望的设计
委托 Delefgation.Composition by reference
把复合下面传入的参数类型改为指针!!!
Handle/Body (pimpl)
图示的这种写法很有名,左边是用户看到的类,里面有调用的接口这些,右边是真正字符串的类,用来封装字符串的类型这些。
reference counting: 这种写法在这个特殊例子当中可以实现,用户创建了三个String对象,但是每个对象下面对应的 rep 指针指向的对象其实是一块内存,因为他们的字符串是一样的,这样就可以减少内存的开销。
继承 inheritance
构造:由内而外 调用父类的默认构造函数,编译器指定的,也符合我们的预期!
析构:由外而内
虚函数:
override 覆写:注意只能用在虚函数这里
non-virtual 函数: 父类不希望子类重写(override)它
virtual 函数:父类希望子类重写(override)它,父类对它一般已经有默认定义
pure virtual 函数(纯虚函数):父类希望子类一定要重写(override)它,父类对它没有默认定义,例子:父类是个抽象类
注意:子类调用父类的函数,如果子类当中对父类的函数进行了override,那么在调用到这个虚函数的时候就会调用子函数覆写的版本,从而实现我们的需求!!!
下面还有个例子:
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void speak() = 0;
};
class Cat : public Animal
{
public:
virtual void speak() { cout << "喵喵喵" << endl; }
};
class Dog : public Animal
{
public:
virtual void speak() { cout << "汪汪汪" << endl; }
};
int main()
{
//用父类对象指针来接受子类对象 来达到子类调用父类对象成员函数的目的
Animal *cat = new Cat();
cat->speak(); // 喵喵喵
Animal *dog = new Dog();
dog->speak(); // 汪汪汪
return 0;
}
用父类对象指针来接受子类对象 来达到子类调用父类对象成员函数的目的,这样也可以实现多态.
转换函数 conversion function
作用:可以用于类型的转换
重载 () 运算符 在括号的前面加上返回的类型,括号内不传参数,返回值由于在括号前面已经指定了,所以省略不写,编译器指定的
operator double(){
return (double)this->_numerator / (double)this->_denominator;
}
整体的例子:
#include <iostream>
using namespace std;
class Fraction
{
public:
Fraction(int num, int den = 1) : _numerator(num), _denominator(den) {}
// 转换函数
// 没有返回值,转化的类型在括号前面已经指定
operator double() const
{
return (double)this->_numerator / (double)this->_denominator;
}
private:
int _numerator;
int _denominator;
};
int main()
{
Fraction f(3, 5);//这里首先创建了f对象,调用了构造函数
double d = 4 + f;
//f是个类对象,他怎么能直接和double类型相加呢?
//如果写了 + 号运算符重载那么就直接调用即可,但是这里没写啊?
//所以这里编译器就去找什么东西可以把f里面的参数变为double 就找到了转换函数 这里的f经过编译之后就返回分数的值 也就是0.6
cout << d << endl;//4.6
return 0;
}
注意main函数里面的细节!!!
函数对象(仿函数) -> 谓词
谓词:
1.函数指针作谓词
2.函数对象(仿函数)作谓词
#include <iostream>
using namespace std;
#include <vector>
#include <algorithm>
// 函数指针作谓词
bool Cmp(int val1, int val2)
{
return val1 <= val2;
}
// 函数对象(仿函数)作谓词
class Fuck
{
public:
bool operator()(int val1, int val2)
{
return val1 <= val2;
}
};
int main()
{
vector<int> nums{5, 3, 4, 2, 1};
// sort(nums.begin(), nums.end(), Cmp);
sort(nums.begin(), nums.end(), Fuck());
for_each(nums.begin(), nums.end(), [&](int val)
{ cout << val << ' '; });
cout << endl;
return 0;
}
仿函数(函数对象): function like classes
在类里面重载 () 运算符
template <class T>
class identity{
public:
const T& operator()(const T& x){return x;}
}
调用的时候 identity() 这样就是一个函数对象
标准库里面有很多仿函数,这些仿函数都继承了一些标准库里面的父类,这些父类大小为0,没有成员函数。(标准库)
模板补充: 成员模板 member template
到目前为止,三种模板: 类模板 函数模板 成员模板
注意看注释进行理解!!!
#include <iostream>
using namespace std;
// Base1 鱼类 Derived1 鲫鱼
// Base2 鸟类 Derived2 麻雀
// 相应的有继承关系
class Base1
{
};
class Derived1 : public Base1
{
};
class Base2
{
};
class Derived2 : public Base2
{
};
// 现在定义一个pair类
template <class T1, class T2>
struct Pair
{
T1 _first;
T2 _second;
Pair();
Pair(const T1 &a, const T2 &b) : _first(a), _second(b) {}
// 注意这里有一个Pair的拷贝赋值
template <class U1, class U2>
Pair(const Pair<U1, U2> &p) : _first(p._first), _second(p._second) {}
// 这里怎么理解
// 如果传入的类型是 T1 鱼类 T2 鸟类
// 然后在调用拷贝赋值的时候传入的类型是 U1 鲫鱼 U2 麻雀
// 显然 鲫鱼是鱼类 麻雀是鸟类 所以是可以传入的
// 这个成员模板需要满足的条件就是 p._first这里是可以给自身的成员属性 _first 进行赋值的,在这里满足的是继承的关系
};
int main() { return 0; }
这里其实就有考虑指针指向的问题
class Derived1 : public Base1 { };
Base1 *ptr=new Derived1;//这么写是完全ok的
还是考虑 Base1 是鱼类,Derived1 是 鲫鱼,然后我们用鱼类的指针去指向鲫鱼对象,这显然是可以的,因为鲫鱼很明显是鱼,所以这么写是完全ok的。
并且恰好这么写可以使得子类调用父类的虚函数来实现不同的虚函数功能。
命名空间
#include <iostream>
using namespace std;
namespace my1
{
static void test()
{
cout << "I am in namespace my1" << endl;
}
}
namespace my2
{
static void test()
{
cout << "I am in namespace my2" << endl;
}
}
int main()
{
my1::test();
my2::test();
return 0;
}
两个命名空间,即使里面的函数名称一样,传入参数等等方面完全一样,甚至还是静态的,虽然静态的存放于全局静态区只有一份,但是这里用了两个不同的命名空间将他们分割开来,这样就导致两个函数本质上是不同的,从下面的使用就可以看出来了。
explicit
non-explicit-one-argument ctor(构造函数)
class Fraction
{
public:
// non-explicit-one-argument ctor
Fraction(int num, int den = 1) : _numerator(num), _denominator(den) {}
//这是上面提到的转换函数
operator double() const
{
return (double)this->_numerator / (double)this->_denominator;
}
private:
int _numerator;
int _denominator;
};
我们先将转换函数去掉,重载加号运算符
class Fraction
{
public:
// non-explicit-one-argument ctor
Fraction(int num, int den = 1) : _numerator(num), _denominator(den) {}
//重载加号运算符
Fraction operator+(const Fraction& f){ ; }
private:
int _numerator;
int _denominator;
};
int main(){
Fraction f(3,5);
Fraction d=f+4;
//到这里的时候编译器发现f和4没办法直接相加 即使写了重载 因为需要传入Fraction类型
//但是编译器看构造函数 默认值den=1 意思是可以传入一个参数,这就和现在的4很贴切了
//所以编译器会将4转化为Fraction类对象和f进行相加得到对象d
}
如果两个同时存在
class Fraction
{
public:
// non-explicit-one-argument ctor
Fraction(int num, int den = 1) : _numerator(num), _denominator(den) {}
//conversion function
operator double() const
{
return (double)this->_numerator / (double)this->_denominator;
}
//重载加号运算符
Fraction operator+(const Fraction& f){ ; }
private:
int _numerator;
int _denominator;
};
int main(){
Fraction f(3,5);
Fraction d=f+4;
//按照上面的思路是一种走法
//但是有了转换函数之后编译器发现,f可以先转化为double数字再和4求和,求完和之后再转化为Fraction对象,这就是另一种思路了
//所以 二义性 报错
}
现在如果加上关键字 explicit 呢?
class Fraction
{
public:
// explicit-one-argument ctor
explicit Fraction(int num, int den = 1) : _numerator(num), _denominator(den) {}
//explict关键字的含义 防止类构造函数的隐式自动转换
//就是说这里由于只需要传入一个参数,所以编译器很可能会把数字隐式转化为Fraction对象
//但是加上了explict之后,明确指出不要让编译器这么干,要生成Fraction对象只能显式调用构造函数!!!!
//conversion function
operator double() const
{
return (double)this->_numerator / (double)this->_denominator;
}
//重载加号运算符
Fraction operator+(const Fraction& f){ ; }
private:
int _numerator;
int _denominator;
};
int main(){
Fraction f(3,5);
Fraction d=f+4;//这里仍然会错,因为4不会被转化为Fraction了,也就没有办法直接相加
double e=f+4;//这里显然就可以了,因为存在转换函数
}
这个关键字 explicit 绝大部分都是用在构造函数前面来防止其他类型的隐式转换!!!
pointer-like classes 关于智能指针和迭代器
智能指针: 用一个类来模拟一般指针的作用
#include <iostream>
using namespace std;
struct Foo
{
void method(void);
};
template <class T>
class shared_ptr
{
public:
shared_ptr(T *p) : _px(p) {}
//智能指针必然需要重载这两个运算符
T &operator*() const
{
return *(this->_px);
}
T *operator->() const
{
return this->_px;
}
private:
T *_px;
};
int main()
{
shared_ptr<Foo> sp(new Foo);
Foo f(*sp);
sp->method();//这个就相当于 _px->method();
return 0;
}
迭代器: iterator 其本质也是一种智能指针
#include <iostream>
using namespace std;
#include <list>
#include <algorithm>
void test()
{
list<int> l{5, 2, 4, 3, 1};
//方法一
//注意这个迭代器类型怎么写的
for (list<int>::iterator iter = l.begin(); iter != l.end(); ++iter)
cout << *iter << ' ';
cout << endl;
// 方法2
for_each(l.begin(), l.end(), [&](int val)
{ cout << val << ' '; });
cout << endl;
}
int main()
{
test();
return 0;
}
specialization 模板特化
对于一个泛型模板,我们调用的时候里面的接口都是一样的。但是如果我们发现有的特殊的类型在某个函数下有更加好的实现方法,这个时候就可以用模板特化来操作了。可以类比子类继承父类(抽象类)的虚函数,特殊化实现,本质是一样的。
#include <iostream>
using namespace std;
template <class Type>
struct Fuck
{
};
// 模板特化
template <>
struct Fuck<int>
{
int operator()(int val) const { return val; }
};
//注意模板特化的语法
template <>
struct Fuck<string>
{
string operator()(string ch) const { return ch; }
};
template <>
struct Fuck<double>
{
double operator()(double val) const { return val; }
};
int main()
{
// 匿名对象
cout << Fuck<int>()(1) << endl;
cout << Fuck<string>()("fuck") << endl;
cout << Fuck<double>()(3.14) << endl;
return 0;
}
模板特化语法第一行要加上,第二行就是具体类型类的具体操作
template <>
class Fuck<type>{
;
};
partial specialization 模板偏特化
个数的偏
template<typename T,typename Alloc= ...>
class Vector{
...
};
//模板偏特化 就只特定其中的某个或者某几个元素 其实还是一个模板
template<typename Alloc= ...>
class Vector<bool,Alloc>{
...
};
范围的偏
#include <iostream>
using namespace std;
template <typename T>
struct TC // 泛化的TC类模板
{
void functest()
{
cout << "泛化版本" << endl;
}
};
// 偏特化:模板参数范围上的特化版本
template <typename T>
struct TC<const T> // const的特化版本
{
// 对特化版本做单独处理
void functest()
{
cout << "偏特化const版本" << endl;
}
};
template <typename T>
struct TC<T *> // T* 的特化版本
{
void functest()
{
cout << "const T*特化版本" << endl;
}
};
template <typename T>
struct TC<T &> // T& 的特化版本
{
void functest()
{
cout << "T &左值引用特化版本" << endl;
}
};
template <typename T>
struct TC<T &&> // T&& 的特化版本
{
void functest()
{
cout << "T &&右值引用特化版本" << endl;
}
};
void test()
{
TC<double> td;
td.functest();
TC<const double> td2;
td2.functest();
TC<double *> tpd;
tpd.functest();
TC<const double *> tpd2;
tpd2.functest();
TC<int &> tcyi;
tcyi.functest();
TC<int &&> tcyi2;
tcyi2.functest();
}
int main()
{
test();
//泛化版本
//偏特化const版本
//const T*特化版本
//const T*特化版本
//T &左值引用特化版本
//T &&右值引用特化版本
return 0;
}