1 函数
1.1内联函数
使用函数有利于代码重用,但是函数调用会降低程序执行效率,增加时空的开销。对于功能简单、规模较小又频繁使用的函数,可以设计为内联函数。
内联函数不是在调用时发生控制转移,而是在编译时将函数体嵌入在每一个调用处。
结构:incline 类型说明符 函数名(签名)
1.2带默认形参值的函数
有默认的形参必须在形参列表的最后,实参与形参是按从左向右的顺序建立对应关系的。
在相同作用域中,不允许在同一个函数的多个声明中对同一参数的默认值重复定义,即使前后定义的值相同也不行。
1.3函数重载
两个以上的函数,具有相同的函数名,但是形参的个数或者类型不同,编译器根据实参和形参的类型及个数的最佳匹配,自动确定调用哪一个函数。
但是,当具有默认形参值的函数重载形式时,需要注意防止二义性。例如:
void fun(int length, int width=2, int height= 33);
void fun(int length);
当调用fun函数时,fun(1)会出现语法错误。
关于函数重载的知识,还涉及运算符重载,重载和多态等,这些会在后面提到。
2 类与对象
2.1面向对象程序设计的基本特点
2.1.1抽象
面向对象方法中的抽象,是指对具体问题进行概括,抽出一类的公共性质并加以描述的过程。
2.1.2封装
封装就是将抽象得到的数据和行为相结合,形成一个有机的整体,也就是将数据与操作数据的函数代码进行有机结合,形成“类”,其中的函数和数据都是类的成员。
通过封装使得一部分类成员充当类与外部的接口,而将其他成员隐蔽起来,这样就达到了对成员访问的权限合理控制,使得不同类之间相互影响减小到最低限度,进而增强数据的安全性和简化程序编写。
2.1.3继承
2.1.4多态
多态性是指一段程序能够处理多种类型对象的能力。在c++中,这种多态性可以通过强制多态、重载多态、类型参数化多态、包含多态四种形式实现。
强制多态是通过将一种类型的数据转换成另一种类型的数据来实现的,也就是类型转换。
包含多态,具体表现是c++中虚函数的实现
参数化多态,具体表现是c++中的模板,分为函数模板和类模板。
2.2类和对象
2.3构造函数和析构函数
2.3.1构造函数
构造函数的作用就是在对象被创建时利用特定的值构造对象,将对象初始化为一个特定的状态。
构造函数在对象被创建时自动被调用。
调用时无须提供参数的构造函数被称为默认构造函数。如果类中没有写构造函数,编译器会自动生成一个隐含的默认构造函数。但是一旦申明了构造函数,编译器就不会再为之生成 隐含的构造函数。
当在自定义构造函数当中带有形参,则在实例化的时候必须给初始值。如:
Clock c1(14,05,02);
2.3.2复制构造函数
其形参是本类对象的引用。其作用是使用一个已经存在的对象(由赋值构造函数的参数指定),去初始化同类的一个新对象。
如果没有定义类的复制构造函数,系统就会在必要的时候自动生成一个隐含的复制构造函数。
构造方法:
class Base{
public:
类名(int b);
类名(Base &base);
private:
int b;
};
被调用的情况:
(1)用类的一个对象去初始化该类的另一个对象时:
int main(){
Base b1(1);
Base b2(b1);//调用
Base b3=b1;//调用
return 0;
}
(2)如果函数的形参是类的对象,调用函数时(若形参是对象的指针或是引用则不会调用):
void fun1(Base b);
void fun2(Base *b);
void fun3(Base &b);
int main(){
Base b1(1);
fun1(b1);//调用
fun2(&b1);//不会调用复制构造函数
fun3(b1);//不会调用赋值构造函数
}
(3)如果函数的返回值是类的对象,函数执行完返回调用者时:
Base g(){
Base b2(2);
return b2;
}
int main(){
Base b2;
b2=g();//调用复制构造函数
return 0
}
2.3.3析构函数
用来完成对象被删除前的一些清理工作。析构函数是在对象的生存期即将结束的时刻被自动调用的。调用完之后,对象也就消失了,相应的内存空间也释放了。
析构函数不接收任何参数,但可以是虚函数。如果不进行说明,系统也会生成一个函数体为空的隐含析构函数。
表示方法,如:
~Base();
3 数据的共享与保护
3.1标识符的作用域与可见性
3.1.1作用域
(1)函数原型作用域:在函数原型声明时形参的作用范围就是函数原型的作用域
(2)局部作用域:函数形参列表中形参的作用域,从形参表中的声明出开始,到整个函数体结束之处。函数体内的声明的变量,其作用域从声明处开始,一直到声明所在的“块”结束为止
(3)类作用域:访问对象成员的基本方法:x.a,X::a,ptr->a
(4)空间命名作用域 如:using namespace std
3.2类的静态成员
3.2.1静态数据成员
采用“static”关键字声明为静态成员。静态数据成员具有静态生存期,不属于任何一个对象,一般用“类名::标识符”,也可以用“对象名.标识符”来访问。
其初始化要在类外进行。如:int Base::count=1;
3.2.2静态函数成员
静态成员函数可以直接访问该类的静态数据和静态函数成员,而访问非静态成员,必须通过对象名。
3.4 类的友元
友元关系通过关键字:friend,提供了不同类或对象的成员函数之间、类的成员函数之间与一般函数之间进行数据共享的机制。是一个类主动声明哪些其他类或者函数是它的朋友,进而给他们提供了对本类的访问特许。
3.4.1友元函数
使用friend修饰的非成员函数,并拥有了可以访问该类的私有保护成员的特权。
在类中只有该友元函数的声明,函数在类外进行定义。
3.4.2友元类
若A是B的友元类,则A中所有成员函数都是B的友元函数,都可以访问B中的私有保护成员。表示:
class B{
friend class A;
};
注意:1.友元关系是不能传递的2.友元关系是单向的3.友元关系是不能被继承的
3.4.3友元成员函数
只有某个类中的特定成员函数具有访问权限如:
class B{
friend A::fun();
};
3.5共享数据保护
使用const修饰
3.5.1常对象
常对象的数据成员值在对象的整个生存期间内不能被改变,因此常对象必须进行初始化且不能被更新。表示为:const Base b(1);
常对象只能调用它的常成员函数,而不能调用其他成员函数
3.5.2用const修饰的类成员
(1)常成员函数:void print() const; 不能更新目的对象的数据成员,也不能调用该类中没有const修饰的成员函数。
(2)常数据成员:const int a,static const int b ;任何函数都不能对该成员赋值,该成员只能通过初始化列表被初始化。
(3)常引用:常引用被应用的对象不能被更新,如函数中不修改传入的值即可用常引用,复制构造函数也可采用常引用。对于比较大的对象而言,值作为形参时,耗时较多,因此使用常引用为宜。
4 数组 指针 字符串
4.1数组
4.1.1数组作为函数参数
使用数组名传递数据时,传递的是地址。形参数组和实参数组的首地址重合,后面的元素按照各自在内存中的存储顺序进行对应,对应元素使用相同的数据存储地址,因此实参数组的元素个数不应少于形参数组元素的个数。
如:void rowSum(int a[][4],int nRow);
4.1.2对象数组
4.2指针
4.2.1指针变量的声明
数据类型*标识符;
如:int * ptr;
4.2.2与地址相关的运算符“*”和“&”
“*”被称为指针运算符,也成为了“解析”,表示获取指针所指向的变量的值,这是一个一元操作符。例如:*ptr表示指针ptr所指向的int型数据的值。
“&”被称为取地址运算符,也是一个一元操作符,用来得到一个对象的地址,例如:&i就可以得到变量i的存储地址
它们出现在声明语句和执行语句中的含义是不同的,与此同时,它们作为一元运算符和作为二元运算符时含义也不同。
一元运算符“*”出现在声明语句中,在被声明的变量名之前,表明是指针如:
int *p;void fun(int *p)
“*”出现在执行语句中或声明语句的初始化表达式中作为一元运算符,表示访问指针所指内容如:cout<<*p;
“&”出现在变量声明语句中位于被声明的变量左边时,表示引用,如:int &rf;void fun(int &rf)
“&”在给变量赋初值时出现在等号右边或者在执行语句中作为一元运算符出现时,表示取地址如:int*p ;int q=&p;
4.2.3指针与const
(1)指向常量的指针:此时不能通过指针来改变所指对象的值,但指针本身可以改变,可以指向另外的对象,如:
int a;
const int *p=a;
int b;
p=&b;//正确,指针可以指向别的地址
*p=1;//错误,不能改变指针指向的内容
(2)指针类型的常量,这时指针本身不能改变,如:
int *const p=&a;
*p=1;//正确,可以修改指针指向的内容
p=&b;//错误,指针不能指向别的地址
4.2.4指针数组
如果一个数组的每个元素都是指针变量,这个数组就是指针数组。指针数组的每个元素都必须是同一类型的指针。声明一维指针数组表示: int *pa[3]
4.2.5指针型函数
指针作为函数的返回值
4.2.6指向函数的指针
函数指针就是专门用来存放函数代码首地址的变量,在程序中可以像使用函数名一眼使用指向函数的指针来调用函数
4.2.7对象指针
(1)对象指针一般概念:指向对象的指针
(2)this指针:是隐含于每一个类的非静态函数的一个特殊指针。用于指向正在被成员函数操作的对象。
(3)指向类的非静态成员的指针
(4)指向类的静态成员指针
4.3动态内存分配
new:语法形式:new 数据类型 (初始化参数列表)
如果内存申请成功,new运算便会返回一个指向新分配内存首地址的类型的指针,可以通过这个指针对堆对象进行访问。如: int *point= new int(0)
在用new建立一个类的对象时,如果该类存在用户定义的默认构造函数,则“new T”和“newT ()”都是正确的,都会调用默认构造函数。
delete:用来删除由new建立的对象,释放指针所指向的内存空间。如果被删除的是对象,就会调用析构函数。new和delete只能一一对应,否则会发生“内存泄漏”。
4.4深复制和浅复制
隐含的复制构造函数并不总适用,因为它只完成浅复制。
在浅复制时,会使两个对象共用同一块内存空间,指向的是同一个内存地址,但是并没有形成真正的副本。并且在执行析构函数的时候,会同一个内存空间会被释放两次,于是导致运行错误。
5继承与派生
5.1类的继承与派生
5.1.1派生类的定义
举例:class Derived :public Base1,private Base2{};
5.1.2派生类生成过程
(1)吸收基类成员,派生类包含了它全部基类中除了构造函数和析构函数之外的所有非静态成员。
(2)改造基类成员:通过继承方式来改造对成员的访问控制。如果派生类声明了一个和某基类成员同名的新成员(如果是成员函数,那么参数表也要完全一样,否则只能称为重载),派生类的新成员就隐藏了外层同名成员。
(3)添加新成员
5.2访问控制
5.2.1公有继承
当类的继承方式为共有继承时,基类的共有成员和保护成员的访问属性在派生类中不变,而基类的私有成员不可访问。
5.2.2私有继承
当类的继承方式为私有继承时,基类的共有成员和保护成员都以私有成员身份出现在派生类中,而基类的私有成员不可访问。
5.2.3保护继承
当类的继承方式为保护继承时,基类的共有成员和保护成员都以保护成员身份出现在派生类中,而基类的私有成员不可访问。
5.3类型兼容原则
class B{}; class D:public B{}
B b1,*pb;
D d1;
(1)派生类对象可以隐含转换为基类对象 b1=d1;
(2)派生类的对象可以初始化基类的引用 B&rb=d1;
(3)派生类的指针可以隐含转换为基类的指针 pb=&d1
5.4派生类的构造和析构函数
5.4.1构造函数
如果对基类初始化时,需要调用基类带有形参表的构造函数时,派生类就必须声明构造函数。
派生类构造函数执行顺序:1.自左向右调用被继承基类的构造函数2.按声明顺序初始化成员对象3.自己。在写构造函数上的顺序无关紧要。
表示:
class derived:public Base2,public Base1,public Base3{
public:
derived(int a,int b,int c):Base1(a),Base2(b),Base3(c)
};
5.4.2复制构造函数
隐含的复制构造函数会自动调用基类的复制构造函数。
5.4.3析构函数
其执行次序刚好和构造函数相反。
5.5派生类成员的标识与访问
我们只能访问一个能够唯一标识的可见成员。如果通过某一个表达式能引用的成员不止一个,成为二义性。
5.5.1作用域分辨符
“::”
如果派生类中声明了与基类成员函数同名的新函数,及时函数的参数表不同,从基类继承的同名函数的所有重载形式也都会被隐藏。如果要访问被隐藏的成员,就需要使用作用域分辨符和基类名来限定。
5.5.2虚基类
当某类的部分或全部直接基类是从另一个共同基类派生而来时,在这些直接基类中从上一级共同基类继承来的成员就有了相同的名称。此时同一个函数名就会有多个映射。因此将共同基类设置成虚基类,这时从不同路径继承过来的同名数据成员在内存中就只有一个副本,同一个函数名也就只有一个映射。
表示:
class Derived :virtual public Base
此时可以通过“对象名.成员名”唯一标识和访问这些成员。
5.5.3虚基类及其派生类的构造函数
当虚基类声明有非默认形式的构造函数时,在整个继承关系中,直接或间接继承的派生类要在派生类构造函数的成员初始化列表中对虚基类进行初始化。
只有最远派生类的构造函数会调用虚基类的构造函数,该派生类的其他基类对虚基类构造函数的调用都会被自动忽略。
6 多态性
多态是指同样的消息被不同类型的对象接收时会导致不同的行为。消息是指对类的成员函数的调用,不同的行为是指不同的实现,也就是调用了不同的函数
6.1运算符重载
6.1.1重载运算符的规则
(1)不能重载 “.”、“.*” 、“::”、 “?:”,这些运算符,且只能重载c++中已有的运算符
(2)不能用友元函数重载:“=” 、“()”、“[]”、“->”
(3)只能用友元函数重载:“<<”、“>>”
6.1.2运算符重载为成员函数
双目运算符:Complex operator+(const Complex &c2)const ;
单目运算符:前置:Complex operator++();
后置:Complex operator++(int )
6.1.3运算符重载为非成员函数
将该函数声明为类的友元函数
双目运算符:Complex operator+(const Complex &c1,const Complex &c2);
单目运算符:前置Complex operator++(const Complex &c1);
后置 Complex operator++(const Complex &c1,int)
6.2虚函数
6.2.1一般虚函数成员
声明语法:virtual void print();
虚函数声明只能在类定义中的函数原型中声明,而不能在成员函数实现的时候声明
判断派生类的函数是否是虚函数的依据:
(1)该函数是不是和基类的虚函数有同样的名称
(2)该函数是否与基类的虚函数有同样的参数签名
(3)该函数是否与基类的虚函数有相同的返回值或是满足赋值兼容规则下的指针、引用的返回值
6.2.2虚析构函数
virtual ~Base();
如果基类的析构函数是虚函数那么由他派生出来的类的析构函数也都是虚函数。
析构函数设置为虚函数时,在使用指针医用时可以动态绑定,实现运行时的多态,保证使用基类类型的指针就能够调用适当的析构函数对不同的对象进行清理。例如在使用new 与delete的时候,如果不将析构函数设置为虚函数,那么会产生不确定的后果(内存泄漏)。
6.3纯虚函数和抽象类
6.3.1纯虚函数
virtual void print()=0;
声明为纯虚函数之后,基类中就可以不再给出函数的实现部分。即便在基类中给出了实现,也必须要被派生类覆盖,否则无法实例化。如果将析构函数声明为纯虚函数,则必须给出它的实现,因为派生类的析构函数执行体执行完后需要调用基类的纯虚函数
6.3.2抽象类
带有纯虚函数的类是抽象类。抽象类派生出新类之后,如果派生类给出所有纯虚函数的函数实现,这个派生类就不再是抽象类因而可以进行实例化。抽象类不能实例化。