1.C与C++的简单区别
1.建立的文件类型不一样,C语言是.c,C++是.cpp
2.引入的头文件不一样
3.C++有命名空间
4.输入输出语句不一样
5.C语言不允许重载,C++可以重载
6.自定义类型,C语言使用struct,C++使用class
7.C语言面向过程,C++面向对象
2.初识类与对象
C++的四个特征:抽象,继承,封装,多态
抽象:对具体事务的定义过程称之为抽象过程
封装:把数据和数据的操作方法捆绑在一起的过程
多态:同一种事务有多种形态的表现
支持继承和多态的语言都称为面向对象的语言
类就是计算机对现实实体抽象类别的一个描述,所创建的实例就是对象
3.类的定义与创建
类是C++中的一种数据类型
C++的对象:类所定义的变量
实例化:定义对象的过程
访问限定符:public,protected,private
如果在类起点没有访问限定符,则系统默认为私有的
访问限定符protected和private体现了类的封装性
类没有空间,所以要先实例化对象,为对象分配空间
4.this指针
类的大小:相应数据成员大小(与字节对齐有关),不包括方法
this指针代表当前的对象
this指针只有在对象调动方法时在函数位置隐藏插入一个this指针。
C++对类的识别顺序:1.类名 2.识别数据成员(无论是公有成员还是私有成员)3.识别方法(函数)所以数据成员的位置可以任意
改写函数:添加一个参数,类名 *const this //用const封锁指针,this的指向不能改变
对象调用这个方法时也会被改写,第一个参数实际上是对象的地址。
5.构造函数和析构函数
只有通过公有的方法才能对私有的数据成员进行访问
构造函数:数据成员多为私有,要对他们进行初始化,必须要用一个公有函数来执行,这个函数仅在定义对象的时候执行一次。
构造函数函数名与类名相同,无返回类型说明,在程序运行时,当新的对象被建立,该对象所属的类的构造函数被自动调用,该生存期也只调用这一次。构造函数可以重载。如果类说明中没有给出构造函数,则C++编译器自动给出一个缺省的构造函数。但是只要定义了一个构造函数,系统就不会生成缺省的构造函数。
区分: Test t1; //实例化对象
Test t1(); //函数声明
析构函数:注销该对象并进行善后工作。
先构造的后析构(构造的对象生成在栈区,所以符合栈先进后出的特点)
析构函数无返回值无参数,一个类只有一个析构函数。
析构函数可以缺省,对象注销时,系统自动调用析构函数。
6.构造函数的三个作用
1.构造对象
2.初始化对象
3.类型转换(与能否生成中间变量有关)
只要类型不一样,永远都不能赋值。
关键字explicit:不允许隐式转换(显示转换关键字)放在构造函数声明之前
7.引用(别名)
引用避免函数的副作用,也避免为实参对象重新分配内存的引起的程序执行效率的大大降低。
引用的几种情况:
1.对变量的引用:
Int a=10;
Int &b=a;
2.对指针的引用:
Int *p=&a;
Int *&q=p;
3.对数组的引用:
Int ar[10]={1,2,3,4,5,6,7,8,9,10};
Int (&br)[10]=ar; //注意不能写int &br[10]=ar; 这样写表示的把数 组赋给是数组元素的引用
不能用非const成员变量引用一个const成员变量:
Const int x=100;
Int &y=x; //错误的示例 正确做法:const int &y=x;但是可以用const成员变量引用一个非const成员变量
4.另一种情况
Const double d=12.34;
Const int &f=d; //可以正常编译,但是行为比较诡异。导致了f和d的地址不是一个。原因是f的地址并非引用了d的地址而是引用了临时变量的地址,而这个临时变量是int类型。这会使程序出现错误。
5.const double d=12.34;
Int& f=d; //程序编译错误,原因是临时变量具有常性,临时变量给f赋值相当于const成员变量给非const成员变量赋值,这是不允许的
8.拷贝构造函数
拷贝只需要拷贝数据成员,而函数成员是公有的
用已存在的对象初始化另外一个对象(没调动构造函数,调动拷贝构造函数)
以类名作为函数名,以自身类型参数的引用做参数
如果把一个真实的类对象作为参数传递到拷贝构造函数,会引起无穷递归,因此要用引用。分析:不使用引用传递而使用值传递时,需要为实参拷贝构造一个临时变量,拷贝构造的实参又需要拷贝构造,因此会出现无穷递归的情景,这会导致栈溢出,使程序崩溃。
易错点:
Test t;
Test t2=t; //注意调用的是拷贝构造函数
Test t3;
t3=t; //注意调用的是赋值函数
拷贝构造函数的三个作用:
1.对象初始化对象
2.当函数的形参是类对象时,调用函数时进行实参与形参的结合时使用,这时要在内存新建立一个局部对象,在返回调用者
函数的返回机制:我们所认为返回的值是局部变量,出了作用域就会消亡。C++和C语言返回的其实是临时对象。即创建一个中间的临时变量,把要返回的值拷贝到临时变量。一旦返回,要返回的那个变量就会消亡(该临时变量的生存周期只在函数调用处的表达式处)。返回的这个临时变量一旦给变量赋值后就会被析构。
临时变量是局部的,返回创建的临时变量。
9.赋值函数
使用引用的好处:1.不再调用拷贝构造函数,效率提高2.不再为其开辟空间节省了内存
常引用的好处:保护参数,使参数的值不能改变
使用引用常忽略的一个问题:分析以下的程序:以引用返回,即不再创建临时的无名对象,返回的就是真实的对tmp的一个引用,但由于tmp是一个临时的对象,一旦函数执行结束,这个tmp就会被析构掉,当主程序中执行一个Test t2;t2=fun(t1)时,由于t1已经析构,所以t2的数据就会变成一个错误的值。因此要注意引用的使用。
Test& Fun(Test x)
{
int value;
value=x.Getdata(); //一个成员函数
Test tmp(value);
return tmp;
}
当返回值不受作用域的影响(如指针)时,考虑用引用
10.函数的调用优化
1.将Test tmp(value);
return tmp;
改为
return tmp(value); //创建了一个临时的无名对象
减少了拷贝构造函数和析构拷贝构造所构造的临时变量的过程(return无名临时对象时减少了一次拷贝和析构,是编译器进行的优化)
2.参数以引用方式传递,不再为参数进行临时对象的拷贝构造,而是直接操作原对象,并且不为参数开辟空间。
3.返回值一引用的方式返回,但是得注意返回值必须不受作用域限制
4.将 Test t1(100);
Test t2;
t2=fun(t1);
改为:
Test t1(100);
Test t2=fun(t1);
t2不是赋值(变成初始化),减去赋值函数的调用,也不用调用拷贝构造了(编译器认为函数返回的临时对象就是t2,因此省去了拷贝构造,是编译器做的优化)
11.深浅拷贝
六个默认的函数:构造函数,拷贝构造函数,赋值函数,析构函数,一般对象的取地址运算符的重载,常对象取地址运算符的重载
如果一个类中数据成员包含指针,一般要重新写拷贝构造函数和赋值函数,光靠默认的拷贝构造函数是无法解决深浅拷贝的问题。
同一个空间不能释放多次
浅拷贝:只是将指针拷贝,而没有做实质的内容拷贝
深拷贝:不只是单纯的只拷贝指针的指向,而是复制出一个相同的空间,并把值传给这个空间
12.运算符重载
C++中不可以重载的运算符为:三目条件运算符,成员.操作符,作用域操作符,sizeof
13.友元
一个常规的成员函数声明描述了三件在逻辑上互不相同的事情:
1.该函数能访问类声明的私有部分
2.该函数位于类的作用域中
3.该函数必须经由一个对象去激活(有一个this指针)
通过将该函数声明为static,可以让他只有前两个性质。通过建一个函数声明为友元可以只让他具有第一种性质(访问类声明中的私有部分)。
友元可以在类的内部实现,也可以在类的外部实现,他的调动不在需要有对象激活。友元在一定程度上破坏了类的封装性。
友元函数注意点:
1.友元函数不是函数,但是友元函数可以访问类中所有成员,一般函数只能访问类中的共有成员。
2.友元函数不受类中访问权限关键字限制,可以把它放在类的公有,私有,保护部分,结果一样。
3.某类的友元函数的作用域并非该类的作用域。如果该类的友元函数是另一类的成员函数,则其作用域为另一类的作用域,否则与一般函数相同。
友元类:整个类可以是另一个类的友元。友元类中的每个成员函数都是另一个类的保护或私有数据成员。
友元函数没有隐藏的this指针
友元函数需要在类外声明(一般运算符的重载都在类外声明)
成员函数与友元函数的区别是:成员少一个参数(this指针)
14.static方法与const方法
通过将函数声明为static,可以让他具有两种性质:
1.该函数内访问类中的私有部分。
2.该函数在类的作用域中。
3.该函数不必经由一个对象去激活。
由关键字static修饰说明的类成员称为静态类成员,虽然使用static修饰说明,但与函数中的静态变量有明显差异。类的静态成员为其所有对象共享,不管有多少对象,静态成员只有一份存于公用内存中。
静态成员分为两种:静态成员数据,静态成员函数
静态成员不属于任何一个对象,而是所有对象所共享。
静态方法只能访问他的静态变量,不能对普通的变量进行访问。
普通方法既可以对普通变量进行访问也可以访问静态变量。
普通方法可以调动静态方法,但静态方法不能调动普通方法。
void list() //只封锁this指针的指向不能改变,但this所指的对象可以修改。
void list()const //常方法,在方法内部不能对数据成员进行任何改变。
将两个函数可以修改为:void list(Test *const this)和void list(const Test *const this)
当数据不能改变时最好把方法写为常方法
常方法不能调动普通方法,普通方法可以调动常方法。
普通方法的指针是可以改编的,当常方法调用普通方法是,普通方法的指针有可能改变,而常方法不允许改变指针指向。
15.模板
模板是不支持隐式转化的,当参数不一致时,模板的使用出现错误。解决方案:
1.Max(1,(int)2.3);
2.Max((double)1,2.3);
3.Max<int>(1,2.3); //指定type的类型
由模板生成的函数称为模板函数,这个工作由编译器做了,因此程序效率并没有提高,只是代码简洁了。
16.动态开辟new_delete
所有的动态存储分配都在堆区进行
静态存储分配在栈区,栈先进后出
堆区也叫自由存储区,手动申请内存,手动释放内存(要注意内存泄露的问题)
new与delete比malloc和free的优点
1.不用计算开辟空间的大小
2.不用强制类型转化
3.不用再去判断空间是否开辟成功。
malloc/free和new/delete不可以交叉使用
new可以在开辟空间的时候进行初始化
malloc和free只负责开辟空间和释放空间,而new不仅开辟空间还调用构造函数,delete先摧毁对象(调用析构函数),然后释放空间
当new创建一个数组,就要求该类有默认的构造函数
动态分配数组的三个特点:
1.变量n在编译时没有确定的值,而是在运行时输入,按运行时所需分配空间。
2.C++不对数组做边界检查,但在对空间分配时,对数组大小是记录在案的。delete[] p;
3.没有初始化,不可对数组初始化,但必须有默认的构造函数,默认值
注意:由堆区创建对象数组,只能调用缺省的构造函数,不能调用任何其他的构造函数。如果没有缺省的构造函数,则不能创建对象数组
17.继承访问属性细说
C++四大特性:抽象,封装,继承和多态
其中继承和多态是面向对象语言的特征
继承机制是面向对象程序设计使代码可以复用的手段,他允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生的新的类,称为派生类。继承呈现了面向对象程序设计的层次结构。体现了由简单到复杂的认识过程。
多态性:多态性是考虑在不同层次的类中,以及在同一类中同名成员函数之间的关系问题。函数的重载,运算符的重载,属于编译时的多态性。以虚基类为基础的运行时的多态性是面向对象的程序设计的标志特征。体现了类推和比喻的思想。
继承的实现主要是为了实现多态性,继承是为了让代码重复使用,由基类派生出派生类的一般形式为:
class 派生类类名:访问限定符 基类名
子类继承父类是把父类共有部分继承下来是错误的,无论是如何继承父类,都会把父类所有成员继承下来,包括私有成员(除构造函数和析构函数)原先父类继承过来的还保持它的访问特性。
三种继承要掌握。
对象只能访问类的公有成员,不能访问私有和保护成员。
19.派生类的构造函数和析构函数的调用顺序(重点)
编制派生类的四个步骤:
1.吸收基类成员:不论是数据成员,还是函数成员,除构造函数和析构函数外全盘接收。
2.改造基类成员:声明一个和某基类成员同名的新成员,派生类中的新成员就屏蔽了基类同名成员称为同名覆盖(隐藏)
3.派生类新成员必须与基类成员不同名,他的加入保证派生类在功能上有所发展。
4.重写构造函数和析构函数
子类对象隐藏包含了父类对象,创建子类对象时,先构造父类,在构造子类。构造父类的顺序是继承父类的顺序
如果子类继承的父类中,如果不提供默认的或者是缺省的构造函数,那我们就要用参数列表的形式针对父类进行构造,这样,这个参数列表的形式就相当于调动父类的构造函数
初始化的顺序与构造的顺序没有关系,构造的顺序只与继承的顺序和声明数据成员的顺序
如果父类有缺省的构造函数,就可以不用参数列表对父类构造。
总结:派生类构造函数各部分执行次序为:
1.调用基类构造函数,按他们在派生类定义的先后顺序,顺序调用。
2.调用成员对象的构造函数,按他们在类定义中声明的顺序,顺序调用。
3.派生类的构造函数体中的操作。
在派生类的构造函数中,只要基类不是使用缺省构造函数都要显式的给出基类名和参数表
如果基类没有定义构造函数,则派生类也可以不定义,全部采用系统给定的缺省构造函数
如果基类定义了带有参数的形参表的构造函数时,派生类就应当定义构造函数
析构函数各部分执行次序与构造函数相反,首先对派生类新增一般成员析构,然后对新增成员对象析构,然后对基类成员析构。
20.钻石继承与虚基类
要访问出现钻石继承问题的成员也要跟上面一样加上作用域访问符访问对象
虚拟继承是解决钻石继承的问题,不会为成员真实的开辟空间,针对公共的访问共同的元素,被继承的父类称为虚基类
在派生类对象创建中,首先是虚基类的构造函数并按他们声明的顺序构造。第二批是非虚基类的构造函数按他们的声明顺序调用。第三批是对象成员的构造函数,最后是派生类自己的构造函数被调用。
21.同名隐藏与赋值兼容规则
函数同名隐藏问题:当父类和子类有同名函数时,当子类对象调用这个函数时,其实调动的是子类的这个方法,这种问题叫做同名隐藏。子类相同名函数隐藏了父类相同名函数(不是多态)
如果在父类里面有一定的函数,在子类中写了一个跟父类同名的函数,子类会隐藏掉父类所有的相同名函数(同名隐藏)
赋值兼容规则的几种情况:
1.派生类的对象可以赋值给基类对象,这时是把派生类对象中从对应基类中继承来的隐藏对象给基类对象。反过来不行,因为派生类的新成员无值可赋。即向上转换,不允许向下转换(对象的切片)
2.可以将一个派生类的对象的地址赋给其基类的指针变量,但只能通过这个指针访问派生类中由基类继承来的隐藏对象,不能访问派生类中的新成员。同样也不能反过来。
3.派生类对象可以初始化基类的引用。引用是别名,但这个别名只能包含派生类对象中的基类继承来的隐藏对象
在任何需要基类对象的地方都可以用公有派生类对象来代替,这条规则称为赋值兼容规则
多态的三同:
名字相同,返回值相同,参数列表相同然后在父类函数的前面加上virtual关键字
父类只能调动父类的方法,哪怕加上了虚关键字virtual
把子类对象地址赋值给父类对象时,哪怕达到了父类的重写方法,也会调用子类的方法
子类对象初始化父类的引用,只要子类重写,我们都会调动子类的方法,这种现象就叫做多态。
22.多态与虚函数的使用
C++的两种多态:
1.编译时的多态:通过函数的重载和运算符的重载来实现(静态多态)
2.运行时的多态:指程序在执行前,无法根据函数名和参数来确定该调用哪一个函数,必须在程序执行过程中,根据执行的具体情况来动态确定。他是通过类的继承关系和虚函数来实现的。目的是建立一种通用的程序。通用性是承租追求的主要目标之一(动态多态)
实现多态的三个条件:
1.有继承关系
2.父类中有虚函数
3.通过父类的指针或引用才能实现
virtual仅用于类定义中,如果该函数要在类外实现,在声明的时候加上关键字virtual,但在类外实现的时候不加。
当某一个类的一个类成员被定义成虚函数,则由该类派生出来的所有派生类中,该函数始终保持虚函数的特征。从父类开始某一函数被声明为虚函数后,在整个继承体系中该函数(函数名相同,参数列表相同,返回值相同)都为虚函数。(与子类函数之前有没有virtual关键字无关)
同名覆盖,如果没有关键字virtual,则是普通的派生类中新成员函数隐藏基类同名成员函数(当然参数表一样,否则是重载),可称为同名异常规则,他不能实现运行时多态。
在派生类中重新定义(重写)虚函数是,不必加虚关键字。但重新定义时不仅要同名,并且它的返回值和参数列表必须与基类中的虚函数一样,否则联编出错
重载、隐藏、覆盖的区别与联系:
隐藏针对继承里面的同名函数,覆盖针对多态。多态争抢了程序的可扩展性,多态用相同的代码达到不同的表现,多态只能用父类的指针或者引用才能实现
成员函数赢尽可能的设置为虚函数,但是必须注意以下几点:
1.派生类中定义虚函数必须与基类中的虚函数同名外,还必须同参数表,同返回值类型,否则被认为是重载,而不是虚函数。如果基类中返回基类指针,派生类中返回派生类指针是允许的(协变)
2.只有类的成员函数才能说明为虚函数。这是因为虚函数仅适用于有继承关系的类对象。
3,静态成员函数,是所有同一类对象共有,不受限于某个对象,不能作为虚函数。
4.一个类对象的静态和动态类型是相同的,实现动态多态性时,必须使用基类类型的指针或者引用,使该指针指向基类的不同派生类的对象,并通过该指针指向虚函数才能实现动态多态性。
5.内联函数每个对象一个拷贝,无映射关系,不能作为虚函数。
析构函数可定义为虚函数,构造函数不能定义为虚函数,因为在调用构造函数时对象还没有完成实例化。在基类中及派生类中都有动态内存分配的内存空间时,必须把析构函数定义为虚函数,实现撤销对象时的多态性。
6.函数执行的速度相对要慢些。为了实现多态性,每个派生类中均要保持相应虚函数的入口地址表,函数的调用机制也是简洁实现。所以多态性总是要付出一些代价,但通用性是个更高的目标。
7.如果定义放在类外,virtual只能加在函数声明的前面,不能再加在函数定义前面。正确的定义必须不包括virtual。
静态函数,内联函数和全局函数都不能是虚函数
要求把析构函数定义为虚函数的原因:在父类指针析构的时候达到一个级联的删除,防止内存泄漏。
23.多态的实现原理分析
虚表指针:在所有成员之前。无论类里面有多少个虚函数,虚表指针只有一个
虚表指针后面有结束标志0,虚表存放虚函数的地址
24.纯虚函数
纯虚函数是指被标明为不具体实现的虚拟成员函数。他用于这样的情况:定义一个基类时,会遇到无法定义基类中虚函数的具体实现,其实现依赖于不同的派生类。
定义春旭函数的一般格式为:
virtual 返回类型 函数名(参数表)=0;
含有纯虚函数的基类是不能用来定义对象。春旭函数没有实现部分,不能产生对象,所以含有纯虚函数的类是抽象类(不能实例化对象,但可以定义相应的指针)。
定义抽象类的目的是要靠具体的类型去继承抽象类,要完全实现抽象类里面所有的纯虚函数。
抽象类的特点是不能实例化
抽象类为所有的具体类别提供一个公共的接口,有利于代码高度的通用化定义纯虚函数注意:
1.定义纯虚函数时,不能定义虚函数的实现部分。即使是函数体为空也不可以,函数体为空就可以执行,只是什么也不返回,而纯虚函数不能调用。
2.“=0”标明程序员肩负定义该函数,函数声明时为派生类保留一个位置。“=0”的本质上是将指向函数体的指针定义为NULL。
3.派生类中必须重新定义的纯虚函数的函数体,这样派生类才能用来定义对象。