C/C++培训
达内IT学院
400-996-5531
继C语言之后,C++也告一个段落了,不过,只是总要串起来才能更有效的掌握,那么现在就来再回顾一下C++的内容,并且将其串起来。
在学习C++之前,我们就用C语言实现了顺序表和链表,这是学习数据结构的基础,提前接触总是好的,在学习c++之后,我们又实现了了两次,分别是使用类实现、使用模板类实现。那么接下来我们就进入C++的第一个主题——类
类
说到类,我就不得不提结构体,当然不是C语言中的结构体,而是C++中的结构体,想想这两者之间的不同:C++中的结构体不仅仅可以存放数据,而且还可以存放函数,不过,我们通常喜欢使用另一个关键字——class
那么现在就再来回顾一下类的定义吧:
class ClassName
{
//……
};
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号。类中的元素称为类的成员;类中的数据称为类的属性或者类的成员数据;类中的函数称为类的方法或者成员函数。
当时在总结类的时候总结了类的留个成员函数,那么我们再来回顾一下,它们分别是:
类的构造函数、析构函数、拷贝构造函数、赋值运算符 重载、取地址运算符重载、const修饰的取地址运算符重载。那么我们重点说一下前几个:
类的构造函数:是一个特殊的成员函数,名字与类名相同,创建类类型对象时,由编译器自动调用,在对象的生命周期内只且只调用一次,以保证每个数据成员都有一个合适的初始值。
构造函数的特性:
1、函数名与类名相同。
2、没有返回值。
3、有初始化列表(可以不用)。
4、新对象被创建,由编译器自动调用,且在对象的生命期内仅调用一次。
5、构造函数可以重载,实参决定了调用那个构造函数。
6、如果没有显式定义时,编译器会提供一个默认的构造函数。
7、无参构造函数和带有缺省值得构造函数都认为是缺省构造函数,并且缺省构造函数只能有一个。
类的析构函数:与构造函数功能相反,在对象被销毁时,由编译器自动调用,完成类的一些资源清理和汕尾工作。
析构函数的特性:
1、析构函数在类名(即构造函数名)加上字符~。
2、析构函数无参数无返回值。
3、一个类有且只有一个析构函数。若未显示定义,系统会自动生成缺省的析构函数。
4、对象生命周期结束时,C++编译系统系统自动调用析构函数。
5、注意析构函数体内并不是删除对象,而是做一些清理工作。
类的拷贝构造函数:只有单个形参,而且该形参是对本类类型对象的引用(常用const修饰),这样的构造函数称为拷贝构造函数。拷贝构造函数是特殊的构造函数,创建对象时使用已存在的同类对象来进行初始化,由编译器自动调用。
在这一个模块里有一个重要的地方,那就是浅拷贝问题,这个在之前也总结过,所以大致回顾一下:
浅拷贝的问题是怎样出现的?又怎么解决这个问题呢?
浅拷贝是经过拷贝之后,两个对象公用一个内存空间,导致释放对象的时候出现内存泄漏的问题。解决的方案有三个,分别是深拷贝的普通版本和简化版本还有写时拷贝。
深拷贝和写实拷贝的方法不同:深拷贝是再开辟一段空间放置拷贝的对象,这样从根本上解决问题,没有出现两个对象共用一块内存,就不会出现内存泄露的问题;而写拷贝是通过计数器的形式来将这块内存空间的使用对象个数写出来,这样释放的时候,通过判断计数器中的数字是不是0来决定是否释放空间。
还有一些点,我通过画图的形式来看一下:
内存管理
在C语言中申请空间是通过malloc、colloc、realloc在堆上申请的,释放空间是通过free。那么在C++中又有什么呢?
在C++中申请空间我们一般使用new,释放空间我们使用delete;如果要申请一组空间,我们使用new[],同样与之匹配的是delete[]。
重点:new和delete、new[]和delete[]一定匹配使用,否则可能出现内存泄露甚至崩溃的问题。
malloc/free和new/delete的区别:
它们都是动态管理内存的入口。
malloc/free是C/C++标准库的函数,new/delete是C++操作符。
malloc/free只是动态分配内存空间/释放空间。而new/delete除了分配空间还会调用构造函数和析构函数进行初始化与清理(清理成员)。
malloc/free需要手动计算类型大小且返回值会void*,new/delete可自己计算类型的大小,返回对应类型的指针。
定位new表达式:定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
继承
1、首先说一下什么是继承:继承是可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展和增进功能。这样产生的新类成为派生类。
2、那么接下来的重点就是继承体系中的基类和派生类了,这两者是继承关系,那么就有继承权限了,继承权限有三种,分别是:public、private、protected。其中public用的最多,在实际运用中使用的都是public。
3、我们在学习类的时候其实学了类的访问权限,也是三个:public、private、protected。在学习继承之前,后两者是没有什么区别的,但是,学习了继承之后,这两个就有区别了,可以说,protected就是为了继承体系而生的。那么我们来看一下他们的区别吧:
基类的private成员在派生类中是不能被访问的,如果基类的成员不想被类外直接访问,但需要在派生类中被访问,就定义protected。
4、然后再来说一下我们在基类和派生类中发现的一个现象,就是同名隐藏了,这和函数重载相似但是不同,首先一个重要的不同点就是作用域不同,同名隐藏发生在继承体系中,基类的一个成员函数和派生类的一个函数的名称相同,然后基类的成员函数在访问的时候就会被隐藏(只是藏起来了,并没有消失)。
既然同名隐藏(重定义)的两个函数的作用域不相同,那么显然就不会构成重载
5、之后就进入这一个模块的额重点了——对象模型
对象模型其实就是类的成员在内存的布局,想要了解继承到底是怎样进行的,首先就要知道各个成员在内存中是怎么分布的。
6、对了还有一点,就是友元关系可以继承吗?答案是否定的,因为友元函数不是类的成员函数。
7、上面的一些理论都是基于单继承说的,有单继承同样也有多继承,说到多继承就不得不再提对象模型了。涉及多继承那么显然继承的基类不止一个,那么,其内存空间又是怎样的布局呢?或者说,继承的先后顺序是什么。经实验验证,继承的顺序是按照写的先后顺序执行的。那么在空间里也就是这样分布的。
8、菱形继承:显而易见,是继承体系呈现的是菱形,如图:
多态
1、多态的分类:多态分为静态多态和动态多态。
静态多态是编译完之后确定所要完成的工作,如:函数重载和泛型编程
说到动态多态就不得不提动态绑定的条件:一是虚函数,基类中必须有虚函数,在派生类中必须重写虚函数;二是,通过基类类型的指针或引用来调用虚函数。
2、说到重写,顾名思义就是将函数重新写一遍,但是也有要求,必须是一个在基类中的虚函数,那么在派生类中必须重写该函数,重写的函数和基类的函数原型相同,不过有一个特例——协变,是重写的特例,基类中返回值是基类类型的引用或指针,在派生类中,返回值为派生类类型的引用或指针。
说到这里,回想起来继承体系中也有函数原型相同的,一个是函数重载,一个是同名隐藏,那么接下来我们就来看一下他们的异同:
3、前面说的虚函数都是在普通的成员函数中定义的,那么,现在我们来看几个特殊的成员函数:
构造函数可以定义成虚函数吗?为什么?
构造函数的作用我们都知道,是创建对象的,而虚函数的调用是通过对象来进行的,这是矛盾的,所以说构造函数不能声明成虚函数。
静态成员函数可以定义成虚函数吗?为什么?
如果定义成静态成员函数,那么在这个函数可以通过类名和域作用符来调用,也就是说,不用创建对象就可以调用。虚表地址的使用必须通过对象的地址才能获取。
析构函数可以定义成虚函数吗?为什么?
如果我们定义一个Base*类型的变量,但是调用的是Derived的构造函数,那么之后清理资源的时候,会出现什么问题呢?
我们会发现,调用析构函数的时候只会调用Base类的析构函数,而不会调用Derived的析构函数,这样就会引起内存泄漏,所以我们将析构函数定义成虚函数,就会解决这个问题。
4、虚表指针——>虚表
模板
之前说静态多态的时候举了两个例子,一个是函数重载,另一个是泛型编程,函数重载我们都清楚,并且还和重定义、重写一起总结过,但是这个泛型编程是什么?
编写与类型无关的逻辑代码,就是泛型编程,而模板就是泛型编程的基础。
模板分为两部份,一部分是模板函数,另一部分是模板类。
首先说一下模板函数,其格式是:
template<typename Param1,typename Param2,…>
返回值类型 函数名(参数列表)
{
……
}
2、实例化,当你写一个通用的加法的时候,如果将通用类型写成T,那么在来调用函数的时候,通过实例化类型来达到计算任意类型的数据的目的,如果你将T实例化成int类型的那么结果也会是int的,同样的,如果将结果实例化成float、char等也会有相应类型数据的结果。
在模板函数推衍过程中不会进行隐式的类型转化
3、模板形参
模板形参在模板形参列表中只能出现一次
模板形参可以是类型形参也可以是非类型形参,所有类型形参前面必须加上class或者typename关键字修饰
定义模板函数时,模板形参列表不能为空
模板类型形参可作为类型说明符用在模板中的任何地方,与内置类型或自定义类型
使用方法完全相同,可用于指定函数形参类型、返回值、局部变量和强制类型转换
在函数模板的内部不能指定缺省的模板实参
显式指定一个空的模板实参列表,该语法告诉编译器只有模板才能来匹配这个调用,而且所有的模板参数都应该根据实参推演出来
4、模板函数的特化:模板函数虽然很强大,可以将代码写完之后带入类型,然后运算该类型的数据,但是并不是所有的类型都适合。我们写的加法的通用函数模板不是不能计算字符串吗?那么该如何解决呢?当然是模板函数的特化了,模板函数特化形式如下:
1.关键字template后面接一对空的尖括号<>
2.函数名后接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参
3.函数形参表
4.函数体
5、模板类
在使用类模板时,需要我们显示的给出模板实参列表,否则编译器无法得知实际的类型。
类模板的成员函数可以在类模板的定义中定义(inline函数),也可以在类模板定义之外定义(此时成员函数定义前面必须加上template及模板参数)。
类模板成员函数本身也是一个模板,类模板被实例化时它并不自动被实例化,只有当它被调用或取地址,才被实例化。
异常处理
1、C语言异常处理的几种方式:
1.终止程序(除数为0)
2.返回一个表示错误的值,附加错误码(GetLastError())
3.返回一个合法值,让程序处于某种非法的状态(坑爹的atoi())
4.调用一个预先准备好在出现”错误”的情况下用的函数(回调函数)。
5.暴力解决方式:abort()或者exit()
6.使用goto语句
7.setjmp()和longjmp()组合
2、按类型捕获:异常是通过抛出对象引发的,该对象的类型决定该激活哪一个处理代码。被选中的处理代码是调用链中与该类型匹配且离抛出 对象最近的那一个
抛出异常后会释放局部存储对象,所以被抛出的对象也就还给系统了,throw表达式会初始化一个抛出特殊的异常对象副本(匿名对象),异常对象由编译管理,异常对象在传给对应的catch处理之后撤销。
3、栈展开:栈展开就是沿着调用链查找匹配的catch语句的过程。
刨出异常的时候将暂停当前函数的执行,开始查找匹配的catch子句。首先查找throw本身是否在try块内部如果是再查找匹配的catch子句;如果有匹配的则处理,没有则推出当前函数栈,继续在函数的栈中进行查找。不断重复上述的过程。如果到main函数的栈,还没有找到,那么终止程序。
4、有时候一个catch子句不能处理一个异常,这时候,就会将这个异常重新抛出,交给更外层的调用链处理
5、异常规范
异常对象的类型与catch说明符的类型必须完全匹配。只有以下几种情况例外
1、允许从非const对象到const的转换。
2、允许从派生类型到基类类型的转换。
3、将数组转换为指向数组类型的指针,将函数转换为指向函数类型的指针。
不要再构造函数和析构函数中抛出异常,否则可能会引起对像的不完整,导致资源泄露。
智能指针
说到智能指针我们就会想到几个:auto_ptr、scoped_ptr、shared_ptr
1、那么我们今天在来回顾一下这几个智能指针,首先说一下auto_ptr,auto_ptr在库中是建议不要使用的,因为它有问题:
一是,已经转移权限的指针要是再次被使用的时候会出现错误,因为那个指针在转移资源之后就被赋成空指针了,要是再使用,通过解引用的方法来赋值 ,显然是不行的,所以就报错了。
二是,拷贝运算符接受了常量。我们知道临时对象具有常性,(也就是不能改变),如果使用拷贝运算符的时候,将一个临时对象附进去,也就是像例子中的那样,那么就会出错。
2、说到scoped_ptr,就不得不说它的特点,那就是霸道,因为,它不允许别人和他共用一块内存空间,也就是不允许拷贝。那么问题来了,如何防止拷贝:
记得以前我们给出了三种方法,再来看一下吧:
在类中给出赋值运算符的重载和拷贝构造函数的声明,但是,不给出定义;
在类中给出赋值运算符的重载和拷贝构造函数的定义,不过给成私有的;
在类中 只 给出赋值运算符的重载和拷贝构造函数的声明,并且给成私有的。
说实话,这三种方案现在再来看,只有第三种还可行,因为现在再来看的话,前两种漏洞很容易就会看出,第一种,因为是public的,我们可以在类外将这个函数实现了;第二种,不能防友元。
shared_ptr:定制删除器 和 循环引用
填写下面表单即可预约申请免费试听!怕钱不够?可就业挣钱后再付学费! 怕学不会?助教全程陪读,随时解惑!担心就业?一地学习,可全国推荐就业!
Copyright © 京ICP备08000853号-56 京公网安备 11010802029508号 达内时代科技集团有限公司 版权所有
Tedu.cn All Rights Reserved