目录
C++的重要特质讲解
一、C++和C的区别
1、面向过程和面向对象
面向过程:加工的是一个个的函数
面向对象:加工的是一个个的类(而类:实现了数据和函数的封装)
2、const
C语言中:通过指针的解引用可以更改原const对象的值,是一个变量
C++:const存放在一个符号表中,是一个符号表达,只有当取地址时,才会另外分配一个内存空间
3、引用
C++中引入了引用的概念,在编译器底层中,引用的实质是const的指针:Type& name------Type* const name。所以(1)引用有占内存,大小和指针大小一样。(2)引用不能改变。(3)引用不是对象(对象:内存空间的别名)
4、inline
C:宏函数,没有语法检查和作用域检查
C++:inline函数,有语法检查和作用域检查
5、C中没有重载,C++中有重载
二、面向对象模型探索
1、类对象的成员变量和成员函数是分开存储的。
2、静态成员变量和静态成员函数都属于类。
3、静态成员函数不包含this指针。
4、普通成员函数包含一个指向具体对象的this指针(即会把当前对象取地址传入,从而当很多对象使用同一块代码时,代码可以区分哪个对象的调用)。
存储:
(1)普通成员变量:存储于对象中,与struct变量有相同的内存布局和字节对齐方式。
(2)静态成员变量:存储于全局数据区中。
(3)成员函数:存储于代码段。
三、面向过程向面向对象的转变
很重要的一个转变是全局函数成员函数的转变,对于成员函数的参数,隐藏了一个this指针,所以在把全局函数改写成成员函数时,可以省掉传入自身对象即可,在函数中若要使用自身对象的变量,直接使用this即可。
面向对象三大概念:
1、封装:将对象属性和对象方法封装成一个类。
2、继承:实现代码复用
3、多态:可以扩展功能
四、对象的构造和析构
关于匿名对象的讲解:(假设Test是一个类,有一个带参的构造函数)
1、Test t1=Test(1,2)
解释:会产生一个匿名对象,即Test(1,2)调用带参构造函数产生一个匿名对象,然后直接将匿名对象转变为t1。注意这里的”=“没有调用构造函数。
2、
Test gg()
{
Test A(1,2); //调用Test的带参构造函数
return A; //采用拷贝构造函数根据A创建一个匿名对象,返回出去
}//析构对象A
解释:执行顺序:带参构造函数拷贝构造函数析构函数
3、
void objplay()
{
gg();
}
void main()
{
objplay();
}
解释:执行顺序:带参构造函数(创建A)拷贝构造函数(创建匿名对象)析构函数(A)析构函数(匿名对象)
4、匿名对象的去和留
void objplay()
{
Test bb=gg(); //匿名对象没有调用析构函数
}
如果用匿名对象直接初始化同一个类型的对象,则匿名对象直接转正,不会被析构,若是采用等号赋值操作,则还是会被析构。
五、深拷贝和浅拷贝问题
浅拷贝与深拷贝示意图:
对于浅拷贝问题:当obj1析构时,堆中对象已不存在,再对obj2对象进行析构,则会造成对同一块内存进行两次析构,所以会报错。解决办法就是采用深拷贝,当拷贝指针时,还得把指针所指向的数据也拷贝一份。
应用场景:
1、拷贝构造函数
当类中有指针变量时,再采用拷贝构造函数时,则会出现浅拷贝问题,所以需要重写拷贝构造函数,即手动为自身对象的指针变量分配内存,再进行拷贝值。例如:Test Obj2(Obj1); (1)根据Obj1的大小分配内存,(2)把Obj1数据复制到Obj2中。
2、等号赋值操作符
同样当类中有指针变量时,再进行等号赋值操作时,同样进行的是浅拷贝操作,需要显式重载等号赋值操作,例如obj2=obj1,(1)先释放obj2的旧内存,(2)根据obj1分配内存大小,(3)把obj1复制到obj2中。
3、move构造函数
move构造函数中传入的是右值,例如:在容器vector中,是以两倍速度成长,即当内存不够时,会重新找块两倍大的内存,进行把旧内存数据拷贝到新内存中,在这里有大量的拷贝操作,速度很慢。而如果采用move操作,则速度会提高很多。move操作即是浅拷贝操作,只是对指针进行拷贝,但拷贝完指针后,必须断掉旧指针,即旧指针赋为null。
三/五原则:拷贝构造函数、拷贝赋值运算符、析构函数、移动构造函数、移动赋值运算符。
六、运算符重载
实质:函数调用
步骤:(1)写出函数名:operator 运算符
(2)写函数参数:根据操作数来写
(3)写函数返回值:根据业务,完善函数返回值(还需注意返回引用还是值)返回引用:可以实现链式编程
1、<<和>>只能进行友元函数重载
2、= 先销毁旧内存,再分配内存,最后copy
3、() 使类像一个函数被调用
4、不要重载&&和|| :实现不了短路规则
七、继承和派生
1、类型兼容原则
(1)基类指针(引用)可以指向子类
(2)可用子类对象初始化父类
2、继承和组合混搭
原则:构造顺序:祖父父类组合自己;析构顺序:相反
3、多继承
(1)有共同祖父
则会出现二义性,可以在父类继承祖父时采用虚继承,则子类只从祖父那继承了一份下来。
(2)两个父类没有共同祖父,而两个父类有一样的成员变量,这时子类在调用这个变量时会产生二义性,这时采用虚继承也没用,只能加作用域解决。
八、多态
1、实现多态的三个条件
(1)有继承
(2)要有虚函数重写
(3)用父类指针(引用)指向子类对象
2、多态理论基础
对函数加上virtual关键字,则会在编译时是动态联编,根据具体对象,执行不同对象的函数,表现多态。如果不加则是静态联编,编译器在编译阶段就决定了函数的调用。
3、虚析构函数
想通过父类指针把子类析构函数都执行一遍,即通过父类指针释放子类资源,但由于析构函数默认是非虚函数,所以不会产生这种效果,需自己把父类析构函数写成虚析构函数。
注意:当父类中有虚函数时,析构函数一般都应该写成虚析构函数。
4、重载、重写和重定义
重载:必须在同一个类中进行
重写:必须发生在子类与父类之间:(1)多态:加virtual;(2)重定义:不加virtual
通过子类调用一个函数时,首先看下子类中是不是有,有则只会在子类中进行查找合适的函数执行,若子类没有则才会在父类中查找,若是通过父类调用一个函数,则先看下是不是虚函数,若是虚函数,可能发生多态,若不是虚函数,直接调用父类中的函数。
5、多态原理探究
1、C++编译器会在对象中添加一个vptr指针,指向虚函数表(虚函数表:存放虚函数指针的数组),通过vptr指针去找各自对应的虚函数表,再找对应的虚函数入口地址,进行迟绑定。
2、三个动手脚地方:(1)提前布局:即添加vptr指针;(2)定义虚函数时:创建虚函数表;(3)调用虚函数时:发生多态,vptr执行的过程。
3、vptr指针是分步初始化的
即在父类的构造函数中调用虚函数,不会发生多态。在执行父类构造函数时,vptr指针先指向父类的虚函数表,等父类构造函数执行完毕后,vptr指针才指向子类的虚函数表。
九、模板即泛型编程
1、函数模板
(1)C++编译器模板机制:编译器从函数模板通过具体类型产生不同的函数,编译器会对函数模板进行两次编译,在声明的地方对模板代码本身进行编译,在调用的对方对参数替换后的代码进行二次编译。
(2)函数模板不会进行自动类型转换,而普通函数会进行隐式的类型转换。
2、类模板
继承中的类模板:模板类派生时,需要具体化模板类;在子类调用构造函数时也需要具体类。
类模板中使用友元函数:
//在类中声明 frinend ostream& operator<< <T> (ostream& out,Test& t1); //在类外定义 typedef <typename T> ostream& operator<<(ostream &out,Test<T>& t1) { }
当重载<<和>>只能用友元函数,此时在类中声明时,函数名后面需要加上<T>。
当类模板遇上static时,每个不同类型的类都有属于自己的static成员,编译器会构造多个具体的类。
十、C++类型转换
1、static_cast<>():编译时会做类型检查,只有当可以隐式转换时,则用这个。
2、reinterpret_cast<>():强制类型转换。
3、dynamic_cast<>():用于继承中父类向子类的转换。
4、const_cast<*>p:要确保p所指向的内存空间能被修改,去掉const属性。
十一、异常处理的实现
C++的异常处理机制使得异常的发生和异常的处理不必在同一个函数中。
抛出类类型异常:
throw Class(); //创建一个对象 try { } catch(Class e) //拷贝给e,会析构两次,先析构异常变量 { } catch(Class &e) //不会拷贝,只析构一次,就是之前创建的对象,最好 { } catch(Class *e) //接受不到异常,类型不同 { } //若是这样: throw new Class; catch(Class *e) //这个e需要自己手动析构 { delete e; }
总结:最好在捕获异常类时采用引用的方法。