**1.为什么要选择c++
##处理相同的逻辑,性能仅次于c语言(性能相近),远远超过其他语言。
##是更好的c语言。c语言是c++的子集,两种语言可以完美兼容。
##面向对象语言,在做大型项目的时候,效率和稳定性远远超过c语言。
##虽然开发效率不如python,lua等语言,但是现在c++11等版本的更新让开发难度降低了很多。**
//----------------------------------------------------------------------------------
2.c++类的
##类的定义: 定义类的关键字有很多种,包括\ 类 class, 结构 struct,枚举 enum,联合 union, 枚举类 enum class
//---------------------------------------------------------------------------------
3.最基本的是 class 和 struct。 两者的区别只有一个,就是默认访问类型,class 是 private struct 是 public
//----------------------------------------------------------------------------------
4.枚举 enum 和 enum class
枚举只能表示整形数字,不加说明的话,可以表示任何一种整数类型
例如:int, long,long long, unsigned int....
既然有类型,也就会有表示的范围。
理论最大值是 unsigned long long 的最大值,范围是[0,18446744073709551615]
理论最小值是 long long 的最小值,范围是[-9223372036854775808, 9223372036854775807]
例如:
enum A
{
AA1 = 18446744073709551615, //合法 但是会有警告,\
当表示的值是[-9223372036854775808, 9223372036854775807]\
显示的加上unsigned long long类型也解决不了
AA2 //不合法,这里推导为unsigned long long类型 AA2 = AA1 + 1超过了最大值
};
enum B
{
BB1 = -9223372036854775808,//合法 但是会有警告
BB2 = 9223372036854775807,// 合法 但是会有警告
BB3, //不合法,这里推导为long long类型,BB3 = BB2 + 1 超过了表示范围
};
//虽然上面的BB3小于可以unsigned long long最大值,\
但是因为前面的值推导成了long long类型,所以不合法
也可以显示的表示
enum A:unsigned long long
{
AA1 = 1,
AA2,
};
//这里的AA1和AA2都是unsiged long long类型
枚举未赋值的值等于上一个赋值的值+1
BB1的类型是B,并不是 long long,当它是负数的时候,就是有符号的,当是正数的时候就是无符号的
例如
unsigned int a = 0;
if (a < BB1) {} //warning 无符号与有符号比较
if (a < BB2) {} //没有警告
enum class 是 enum 的升级,enum class 的使用是有自己的作用域的,不会有命名冲突,\
所以使用的时候也需要加上域解析附::
例如:
enum A
{
AA
};
enum B
{
AA //错误,因为A中定义了AA
};
enum class C
{
AA = 1 //对的,因为它在自己的域里面
};
std::cout << AA << "--" << C::AA << std::endl;
//打印0--1,C::AA的访问需要域解析附
用宏定义冲掉域解析附
#define AA 101
std::cout << AA << "--" << C::AA << std::endl;
// 100--1 这里宏定义会把AA替换掉,但是先定义宏的话,会有命名冲突
// 这是什么鬼神仙代码,我只是好奇写了一下,居然编过了,可以思考一下宏定义的原理
有一个使用技巧,例如
enum class SON
{
EMPTY,
AAA,
BBB,
ERORR_SON
};
//例如在枚举类里加儿子,当判断的时候,儿子的范围肯定在EMOTY-ERROR_SON之间,\
判断的代码不用改,只需要在枚举里加儿子就行.
//枚举的使用总结
枚举和宏定义一样,是为了将特殊的数字类型值用一个名字替代,\
当然最好是连续的,这样看上去更有意义。
枚举最好表示的数字要小一些,太大的数字用枚举没有意义。
枚举的成员一定是整形数字,但是不是数字类型,\
建议使用 enum class,这里可以让代码更严谨。
//----------------------------------------------------------------------------------------
5.联合类型 union
一个特殊的类型,整个联合内,只能有一个值被赋值,其他的都会被覆盖,因为所有的成员都用同一个地址
例如
union A
{
long long a;
int b;
};
A test;
test.a = 100;
test.b = 200;
std::cout << "a = " << test.a << "b = " << test.b << std::endl;
// a = 200 b = 200 因为&test.a = &test.b
联合的大小是成员里最大的一个决定的。
// sizeof(A) = 8;// long long决定的
联合里不能有带有静态成员,构造,析构,赋值操作符的类
例如
union B
{
int a;//合法
std::string b;//不合法
}
联合默认访问类型是 public,可以定义成 private
例如
union C
{
private:
int a; //a不可访问,一般不这么干
};
匿名联合是另一种联合方式,因为没有空间,所以里面的成员是直接使用的,并且性质与非匿名联合是一样的。
例如
union
{
int a;
char b;
};
a = 10;
b = 20;
//都是合法的,直接用就行
联合的应用
##判断计算机的大端方式和小端方式
union
{
char a[2];
unsigned int b;
};
b = 1;
std::cout << "a[0] = " << int(a[0]) << "b[0] = " << int(b[0]) << std::endl;
//因为b的二进制数为0000000000000001,\
小端方式为正常的0000000000000001 (高内存地址存放低位数据) \
大端方式位相反的1000000000000000 (高内存地址存放高位数据)
//所以a[0] = 0 a[1] = 1为小段方式,反之为大段方式
##数据压缩
当传输数据或者需要压缩数据的时候,需要将数据转换成 char[]类型,\
可以定义一个联合类型,将数据做简单的压缩。
例如
union A
{
char a[8];
long long b;
};
b = 100000000;
sendMsg(A.a,sizeof(A)); //假设参数是(char* buf,uint8_t buf_size)
//-------------------------------------------------------------------------------------
6.类的大小
#类的大小存在内存对齐情况,类的大小受最大的成员变量影响
例如
class A{
int a;
long long b;
};
sizeof(A) = 16;
// 受long long影响,int不够8个字节,后面四个字节空省
#成员函数不占空间
例如
class A{
int a;
int p() {return 0;};
};
sizeof(A) = 4;
// 只有a占4个字节
#任何类都有自己的空间
例如
class A{};
sizeof(A) = 1;
// 类的最小空间为1个字节
#虚函数列表占8个字节
class A
{
virtual void p() = 0;
};
sizeof(A) = 8;
//里面只有虚函数列表,是一个虚函数映射的指针,用于映射子类的虚函数关系
//-------------------------------------------------------------------------
8.类的访问权限
允许外部访问 public protected
不允许外部访问 private
public 和 protected 在类内作用相同
继承后关系如下
public -> private = private
public -> protected = protected
public -> public = public
private -> private = private
private -> public = private
private -> protected =private
protected -> public = protected
protected -> protected = protected
protected -> private = private
##应用
通过成员权限的限制,来限制客户程序员对类的使用修改权限,以维护项目的稳定性
//以class A举例说明
class A
{
int xx;
};
1.类的默认成员函数
类的默认成员函数有构造函数A() {},析构函数~A(){},\
拷贝构造函数 A(const & A a) {this.xx = a.xx;}
默认赋值函数 A& operator =(const& A a) {this.xx = a.xx; return *this;}
c++中类的设计最重要的一点就是需要深入了解类的默认函数还有各种操作分别调用了哪些函数\
一个好的c++程序员,需要通过自己对代码的理解,减少计算的开销。
//-----------------------------------------------------------------------------------------
#构造函数有多种形态,首先是默认构造函数A() {};函数定义后就会自动生成默认构造函数,\
正常定义成员的时候,会调用默认构造函数。
例如:
A a; // 调用A() {}
构造函数不能定义成虚函数,如果定义成虚函数编译不过。主要原因是类的定义需要先构造基类
#析构函数 ~A() {},类被释放的时候,会自动调用析构函数。
析构函数可以定义成虚函数,如果定义成虚函数,会先释放子类,再释放基类,这样可以将申请的指针是放干净,\
如果不定义成虚函数,则会存在内存泄露问题,具体原因放到后面讲一下。
//-----------------------------------------------------------------------------------------
#自定义构造函数,构造函数也可以自定义,例如A(int xxx) {xx = xxx;}
例如:
A a(100); // 会调用A(int xxx) {xx = xxx};
//这里值得注意的是,当自定义构造函数后,原来的默认构造函数会被覆盖。
例如:
class A
{
public:
A(int xx) {xx = xxx;}
int xx;
};
A a; // error,原有的A() {}已经被覆盖,
A b(100); // 对的,调用A(int xx) {xxx};
//如何解决这个问题
class A
{
public:
A() {}
A(int xxx) {xx = xxx;}
int xx;
};
这样就解决了,或者引用c++11提供的新方法
class A
{
public:
A() = default;
A(int xx) {xx = xxx;}
int xx;
}
// c++11的东西前面一笔带过,后面单独讲。
//---------------------------------------------------------------------------------
#拷贝构造函数 A (const A& a) {this.xx = a.xx;}
例如
class A
{
public:
A() {};
A(int xx) {xx = xxx};
A(const A& a) {this.xx = a.xx;}
int xx;
};
用一个变量给另一个变量赋值的时候,会调用拷贝构造函数.
例如:
A a; // A()
A b(a); // A(const A& a)
A c = a; // A(const A& a);
//这里需要注意的是,这里的赋值也是调用的拷贝构造函数,并不是等号赋值
//---------------------------------------------------------------------------------
#等号赋值运算符 A& operator =(const A& a) {this.xx = a.xx;}
等号赋值运算符是已经定义的变量做等号赋值的时候调用的
例如:
class A
{
public:
A() {};
A(int xx) {xx = xxx};
A(const A& a) {this.xx = a.xx;}
A& operator =(const A& a) {this.xx = a.xx;}
int xx;
};
A a; // A()
A b = a; // A(const A& a)
b = a; // A& operator =(constA & a)
//----------------------------------------------------------------------------------
#初始化列表 A(int xxx):xx(xxx){}
初始化列表的构造函数性能来说会高于普通的自定义构造函数,可以减少一次默认构造函数的调用
例如:
class A
{
public:
A() {};
A(int xx) {xx = xxx};
A(const A& a) {this.xx = a.xx;}
A& operator =(const A& a) {this.xx = a.xx;}
int xx;
};
class B
{
public:
B(A& a):aa(a) {};
A aa;
};
A a; // A()
B b(a); // A(const A& a)
//这里值会在A a;调用一次A()默认构造,在 aa(a)调用一次拷贝构造,而A aa的构造则通过初始化省略
class B
{
public:
B(A& a) {aa = a.aa};
A aa;
};
A a;// A()
B (a); // A() A() A& operator =(A& a)
//这里会用2次构造函数,初始化的时候,成员变量A aa也会初始化一次,再调用赋值函数=()
//使用总结
成员中有常量的时候只能通过初始化列表初始化。
例如:
class A
{
public:
A(int aa): a(aa) {} // 正确,初始化常量
A(int aa) {a = aa;} // 错误,常量是只读的不能写操作
const int a; //声明一个常量
};
成员中有引用的时候
class A
{
A(int aa): a(aa) {} // 正确,相当于给引用赋值,不过这个引用的和int aa并没有联系
A(int aa) {a = aa;} // 错误,
int& a; //声明一个常量
};
成员中没有默认构造函数的时候
class A
{
public:
A(int a) {aa = a;} //默认构造函数被覆盖
int aa;
};
class B
{
public:
B(A a) : c(a) {} //正确
B(A a) {c = a;} //错误,没有可以调用的默认构造函数
A c;
};
//------------------------------------------------------------------------------------
##构造函数 析构函数的调用顺序和类的继承关系
先举个例子
class A
{
public:
A() {};
~A() {};
};
class B : public A
{
public:
B() {}
~B() {}
};
// 常规的用法
B* b = new B();
delete b;
// 调用顺序是 A() B() ~B() ~A() 完全没问题
// 工厂模式用法
A* b = new B();
delete b;
// 调用顺序 A () B() ~A() 少了一次析构,因为析构函数不是虚函数,没办法调用到,\
而构造函数会再A*b 和 B()中进行构造
将析构函数定义成构造函数
class A
{
public:
A() {};
virtual ~A() {};
};
class B : public A
{
public:
B() {}
~B() {}
};
// 工厂模式用法
A* b = new B();
delete b;
// 调用顺序 A () B() ~B() ~A() 正确,构造顺序正确,析构的时候会先调用~B(),释放掉后再自动调用~A()
//------------------------------------------------------------------------------------
2.重载运算符
类只有一种默认运算符 赋值运算符=,如果想要根据自己的要求来定义运算符,则可以重载运算符的函数 \
返回值 operator 符号(参数) {实现; return 返回值;}
运算符的重载大大的提高了代码的简洁性和可读性,是一种非常重要的工具,灵活使用运算符的重载,是写\
基类的一项重要技能,可以阅读stl的一些源码去了解运算符的重载。
其中成员运算符. 指针运算符-> 条件运算符?: 域解析附:: 长度运算符sizeof()不能重载
3.函数的重载,覆盖
##重载
函数的重载指的是函数名相同,参数不同,通过传入不同的参数进行推导,进而可以调用到不同的函数。
例如:
int p(int a, int b) {return 0;} //正确
void p(int a) {} //正确
int p(int a) {return 0;} // 错误,参数和函数名不能都相同
p(100); //调用void p(int a) {}
p(100,200);//调用int p(int a, int b) {}
##覆盖
覆盖就是子类重写了基类的虚函数,进而实现了通过基类指针调用到子类函数的目的,实现了类的多态。
例如
class A
{
public:
virtual void p() {std::cout << "A::p()" << std::endl;};
void p1() {std::cout << "A::p1()" << std:;endl;}
};
class B : public A
{
public:
virtual void p() {std::cout << "B::p()" << std::endl;}
void p1() {std::cout << "B::p1()" << std::endl;}
};
A* p = new B();
p->p();// 输出B::p(),这里子类把基类覆盖了。
p->p1(); // 输出A::P1(),这里调不到子类的方法
重写、重载、覆盖:https://blog.csdn.net/m0_50313114/article/details/124902623
//--------------------------------------------------------------------------------------------
3.虚函数及其原理
虚函数是c++最重要也是最强大的工具之一,知道虚函数的工作原理是一个合格c++程序员的基本。
编译时,由编译器创建虚函数表,编译器给每个包含虚函数的类创建一个虚函数表(vtable),\
虚函数表中的元素存放该类中所有虚函数的地址
创建类对象时,编译器为每个包含虚函数的类对象提供了一个虚函数表指针(vptr),\
虚函数表指针指向该对象所属类的虚函数表
程序运行时根据对象的类型初始化虚函数表指针,使之指向所属类的虚函数表,\
从而在调用虚函数时,就能够找到正确的函数
虚函数表是由编译器自动创建和维护的,每个包含虚函数的类对象都会有一个虚函数表指针,\
即虚函数表是和类对应的,而虚函数表指针是和类对象对应的
虚函数表指针通常作为第一个类成员被初始化,由编译器自动进行初始化操作
//以上总结是书面性的一些东西,面试的时候会问的比较多,但是更重要的是自己要对虚函数有自己的理解
##虚函数的作用
指针和引用调用虚函数的时候,会在调用的时候确认调用子类还是父类的函数。普通变量只能调用自己的函数。
例如
class F
{
public:
virtual void p() {std::cout << "F::p()" << std::endl;}
};
class S : public F
{
public:
virtual void p() {std::cout << "S::p()" << std::endl;}
};
F f1;
f1.p();// F::p() 普通变量
F f2 = F(S());
f2.p();// F::p() 普通变量
F* f3 = new S();
f3->p();// S::p() 指针变量,初始化的时候子类将虚函数做了映射,覆盖了父类的对应函数
S s;
F& f4 = s;
f4.p(); // S::p() 引用变量,初始化的时候子类将虚函数做了映射,覆盖了父类的对应函数
F* f5 = new F();
f5.p();// F::p() 初始化的时候映射关系决定的
// 从上面的几个例子可以简单的知道虚函数的基本用法
#调用指定版本的函数
很多时候,我们想效用父类的虚函数,但是指针定义的是子类,或者在子类里调用父类方法,可以用域解析附来处理。\
这也是很多项目在原有方法上扩充的一个利器。
例如
F* f = new S();
f->F::p();// F::p() 这里可以调用到父类的p();
再例如
void S::p()
{
F::p();
//增加其他处理
std::cout << "S::p()" << std::endl;
}
f->p(); // F::p() S::p() 方法的继承不一定要完全覆盖,也可以追加逻辑。
#纯虚函数
父类的虚函数不做实现,在声明的时候加上=0的标识,则这个函数是纯虚函数,\
子类想继承有纯虚函数的父类,一定要覆盖父类的纯虚函数
例如
class F
{
public:
virtual void p() = 0;
};
class S : public F
{
public:
virtual void p() {}
};
// 上面的F有纯虚函数p(),S想继承的话,一定要实现p(),如果不实现,则无法编译过
在写基类的时候,如果这个基类有一定要让使用者做的工作,那这个接口定义成纯虚函数即可。
#虚函数的安全和限制
有的时候,继承虚函数的时候,会出现笔误,但是这种错误又不容易发现,\
所以建议在覆盖虚函数的时候,在后面加上 override 关键字
例如:
class F
{
public:
virtual void p(){}
};
class S : public F
{
public:
virtual void p() override {} //正确
virtual void p1(){} //正确,但是如果目的是为了覆盖p(),则是错误的,因为书写错误但是自己不知道
virtual void p2() override {} //错误,因为父类没有p2()的虚函数
};
虚函数还有一个进行限制的关键字 final,当虚函数声明称了 final,则不允许被覆盖
例如
class F
{
public:
virtual void p1() final {}
virtual void p2() {}
};
class S : public F
{
public:
virtual void p1() {} // 错误,禁止覆盖
virtual void p2() {} // 正确
}
当你设计类的时候,如果你已经把这个类的方法实现了,不想其他使用者覆盖,则可以加上 final 关键字
##虚函数需要注意的地方
子类对父类虚函数进行重写的时候,可以省略掉 virtual 关键字,只要保证函数名,返回值,参数相同即可。
虚函数覆盖前后,访问限定符没有限制。
只有引用变量和指针变量才会让虚函数指针映射虚函数列表。
//---------------------------------------------------------------------------------------------
5.类的关键字和设计
类设计的核心就是面向对象的思想,封装,继承,多态。
##封装
类的封装是一个复杂的问题,涉及了类的设计,实现的封闭,接口的安全,\
数据的保护,还有给客户程序员提供的权限。
#设计
这里的设计主要是类本身的属性还有根据功能需求来设计整个类。关于类的设计,建议学习一下设计模式\
常见的设计模式如果能学明白并用c++实现的话,就可以成为一名合格的高级软件工程师。
我这里根据自己的经验,总结了一部分设计的技巧和方法。
隐藏构造:
目的是为了保证类成为一个独一无二的对象,可以看一下单例模式
将构造函数和拷贝构造函数重载,并用 private 限定符限制访问权限
虚函数:
目的是为了让子类可以重写这个方法,用父类指针调用相同的函数,实现不同的结果,主要是工厂模式用的多一些。
将可以扩展或者修改的方法,定义成虚函数,子类如果想扩展就覆盖它,不想扩展则直接继承。
纯虚函数:
目的是让每个继承父类的子类都做相同性质的工作。外部再用指针访问这个纯虚函数。很多线程基类的设计就会如此。
例如:
class BaseProcess
{
public:
void run(){/*起一个线程跑exec*/}
void stop() {/*停掉这个线程的工作*/}
virtual void exec() = 0;
};
class Process:public BaseProcess
{
public:
virtual void exec() {std::cout << "exec()" << std:;endl;}
};
// 这样就杜绝了要用一个没有操作的方法。
#接口的处理
提供接口的时候,一定要做到低耦合,高聚合,同一个函数只处理同一间事情,\
成员变量提供修改和访问的接口,并根据需求对权限进行限制。
例如
enum class Sex
{
Boy,
Girl
};
class Son
{
public:
Son():m_age(0),m_sex(Boy) {}
public:
void birthDay() {setAge(getAge() + 1);}
uint32_t getAge() {return m_age;}
Sex getSex() {return m_sex;}
private:
void setAge(uint32_t age) {m_age = age;}
private:
uint32_t m_age;
Sex m_sex;
};
//外部不能直接修改Son类的m_age,但是可以通过getAge()得到m_age,可以通过birthDay()间接修改m_age;
//这个简单的类不建议再birhDay()中写m_age++;这样的代码,这样不符合高聚合的原则,
//同样,也不建议再除了getSex()之外的接口中出现m_sex变量,这样不符合低耦合原则
//根据类本身的性质,给访问权限加修饰,不建议成员函数设置成public,因为真的不安全,除非特殊原因。
#数据的安全
在封装方法发时候,有很多情况是不想任何数据被修改的,则可以使用 const 关键字,
例如
class Son
{
public:
uint32_t getAge() const {return m_age++;} // 错误,m_age被修改
private:
uint32_t m_age;
};
// 保护了m_age的安全,避免了错误的修改
参数的安全和效率也是要考虑的一个问题,例如
class Father
{
public:
void eat(Son& son) {son.birthDay();}
};
//这里调用了eat之后,参数son发生了改变,这是我们不想发生的,所以可以做以下修改
void Father::eat(const Son& son)
{
son.birthDay(); // 错误
son.getAge();// 正确
}
// 思考去掉&可以吗,优缺点是什么
#关于封装的一些经验只谈
尽量用初始化列表对数据进行初始,这样可以减少一部分开销。
成员变量最好不要直接暴露出来,而是给访问的接口。
每个成员变量都提供对应的接口,这样可以在做调整的时候,尽量少的修改代码。
写接口的时候多去使用 const 关键字和 & 引用符,提高效率,还能保护变量和参数安全
使用访问限定符限制访问权限,保护数据是类设计最重要的一部分之一。
对类进行分析的时候,精简基类,减少子类的开发量
//---------------------------------------------------------------------------
##继承
继承中大多数东西已经在前面的篇章中讲了,主要是继承后访问权限,构造析构调用的顺序,虚函数的映射关系等。\
这些都是最基础的部分,还有一些同样重要的部分,需要我们去思考的,也是设计复杂类必修的一部分。
#多继承 一个类继承多个类
例如:
class A
{
public:
int a;
};
class B
{
public:
int a;
};
class C: public A, B {};
//开始思考一下,c这个类是否合法,大小如何
// 当前来说,这个定义肯定是合法的,当然他的基类都有int a,大小是8,两个int大小。
// 再看一下下面这个
class C : public A, B
{
public:
C(int cc): a(cc) {}
};
// 上面这个是错的,编译器不知道用哪个a,也没有定制对应的匹配规则,所以会有二义性错误。
消灭二义性
class C : public A, B
{
public:
C(int cc): a(cc) {}
int a; //定义一个自己的a,所以其他的a就被覆盖掉了,但是这个a已经和A,B的成员不是一个a了
};
// 二义性被解决了,但是同样问题来了,这个C类大小是多少?:答案是12,每个类都有自己的a
// 设计类的时候不建议这样做,但是很多继承过程中难免会出现对应的问题。为了解决二义性分歧,\
不建议通过这种方式,建议用域解析附单独处理对应的变量
例如
class C : public A, B
{
public:
C(int aa, int bb){A::a = aa; B::a = bb} //通过域解析附解决二义性,但是C并没有自己的a
};
// 二义性同样存在于函数,将上面的int a替换成int a(){}也同样成立
多重继承的类构造和析构顺序
// A() B() C() ~C() ~B() ~A()
需要注意的是,最好避免多重继承的二义性,因为这样父类的成员就失去了它本身的意义了。
#虚继承 多重继承中,父类有共同的父类,为了消除二义性,可以使用虚继承的方式
例如
class Base
{
public:
int a;
};
class A : virtual public Base
{
};
class B : virtual public Base
{
};
class C : public A, B
{
};
C c;
c.a = 10; // 正确 并且sizeof(C) = 24;
//看下面的类的大小
sizeof(Base) = 4; //int
sizeof(A) = 16; // int + 偏移指针
sizeof(B) = 16; // int + 偏移指针
sizeof(C) = 24; // int + 偏移指针 + 偏移指针
虚继承有一个虚继承偏移指针
C的结构如下
-----------------
Base int| |
A 偏移指针|
B 偏移指针|
-----------------
所以有如下访问
c.a;//正确 可以访问到 int a
c.Base::a;//正确 可以访问到int a
c.A::a;// 正确,可以访问到int a
c.B::a;// 错误,内存发生偏移,公共部分没法访问
##多态
c++的多态有2个方向,一个是基类指针或者引用调同一个函数得到不同的结果,这个是通过虚函数实现的。\
另一个多态是调同一个函数传入不同参数得到不同的结果,这个是通过函数重载实现的。
上面已经讲了很多虚函数了,但是这里又要再一次讲,可以证明虚函数的重要性。\
这里写一个简单的工厂,来简单说一下多态,虚函数的原理不多加赘述。
例如
class Fish
{
public:
Fish() = default;
virtual void pop() {std::cout << "ooooo" << std::endl;}
std::string getName() {return m_name;}
std::string m_name;
};
class RedFish : public Fish
{
public:
RedFish() = default;
virtual void pop() {std::cout << "OOO0OOO0ooo" << std::endl;}
RedFish(std::string name) {m_name = name;};
};
class GreenFish : public Fish
{
public:
GreenFish() = default;
virtual void pop() {std::cout << "O0ooo" << std::endl;}
GreenFish(std::string name) {m_name = name;};
};
class FishFactory
{
public:
Fish* product(std::string str) {
if (str == "red") {
return new RedFish(str + "_fish");
} else if (str == "green") {
return new GreenFish(str + "_fish");
} else {
return NULL;
}
}
};
FishFactory factory;
auto fish1 = factory.product("red");
if (fish1) {
std::cout << fish1->getName() << std::endl;
std::cout << fish1->pop() << std::endl;
}
auto fish2 = factory.product("green");
if (fish2) {
std::cout << fish2->getName() << std::endl;
std::cout << fish2->pop() << std::endl;
}
// 这里调用getName(),得到了red_fish和green_fish因为本身属性不同
// 调用pop(),得到不同的打印,是因为虚函数调用了不同的函数
// 这两者有本质的区别
再就是函数的多态,通过调用相同的函数,传入不同参数,得到不同的结果。
例如
class Fish
{
public:
void jump() {std::cout << " jump" << std::endl;}
void jump(uint32_t high) {std::cout << "jump " << high << "m" << std::endl;}
};
Fish f;
f.jump();
f.jump(100);
//调用相同的函数,得到不同的结果。
5.常用技巧
#通过域解析附部分继承虚函数
例如
class Father
{
public:
virtual goHome() {std::cout << "eat food" << std::endl;}
};
class Son : public Father
{
virtual goHome() {Father::goHome(); std::cout << "sleep" << std::endl;}
};
Son son;
son.goHome(); // eat food sleep
这样,在Son的虚函数里,加上Father::goHome(),这样既能保留父类的方法,也能在此基础上增加子类的特性。
这种情况比较适用于相同接口功能有重叠或者子类会在父类补充操作的情况。
#调用自己的纯虚函数
例如
class Process
{
public:
virtual void exec() = 0;
void run() {exec();}
};
class AProcess: public Process
{
public:
virtual void exec() {std::cout << "AProcess" << std::endl;}
};
class BProcess: public Process
{};
Process p; //错误 有纯虚函数的类不能直接定义
Process* p = new BProcess();// 错误,没有实现exec()
Process* p = new AProcess();// 正确
这种情况适用于写纯基类,就是只提供基础属性,不是任何实体的,类似于动物,猫科这种,\
在没有具体实现特征都只是类,不是对象,更不能实例化。
#异常处理
c++的异常处理通常使用 try{throw} catch{}
一场处理的好处是,遇到可能导致崩溃的问题时候,还可以在捕获异常的时候,处理一些棘手的问题,\
比如保护数据,定位问题
例如
try
{
int a = 0;
int b = 10;
if (a == 0) {
throw a;
}
std::cout << "a" << std::endl;
}
catch (float e) {
std::cout << "error e = " << e << std::endl;
}
catch (int e) {
std::cout << "error e = " << e << std::endl;
}
//因为前面捕获了a == 0的异常,这个时候会被catch (int e)捕获,打印error e = 0
//如果这个时候有数据未保存,则可以在catch中加上写库的操作
//也可以通过捕获的问题精准确定bug的位置
//再就是任务崩溃的时候,可以在catch上,纠正错误,重新拉起。
// 例如
uint32_t g_run = 10;
void test()
{
if (g_run == 0) {
throw g_run;
}
std::cout << "倒计时 " << g_run << std::endl;
}
try
{
while (g_run--) {
test();
sleep(1);
}
}
catch (uint32_t e)
{
g_run = 10;
while (g_run--) {
test();
sleep(1);
}
}
//在catch里面,test的操作会被重新拉起,当然,要是再次崩溃就没办法了,除非再嵌套一层