C++期末复习小整理(三)

C++课上整理及思考

1.绪论
C++融合了三种不同的方法,

面向过程的编程:C++继承了C语言高效、简洁、快速和可移植性的传统
(自顶向下、逐步求精结构化编程技术反映了过程性编程的思想,根据可执行的操作来构思一个程序)
基于对象和面向对象的编程:C++在C语言基础上添加,C++的核心概念就是类
(强调数据,让语言来满足问题的要求 类和对象 自下向上(bottom-up)编程
有助于创建可重用的代码)
泛型编程:C++模板特性支持
(强调算法 创建独立于类型的代码)

C++与C保持兼容,C++不是一个纯正的面向对象的语言

类的分类:
抽象类:
含有纯虚函数的类被称为抽象类。抽象类只能作为派生类的基类,不能定义对象,但可以定义指针。
抽象类为类族建立一个公共的接口,更好的发挥多态性

基类

派生类

面向对象程序设计的特点
封装性
继承性
多态性

继承对于软件复用有着重要意义,是面向对象技术能够提高软件开发效率的重要原因之一。

2.简单程序设计
字符集:

字母
包括大写英文字母A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
和小写英文字母a b c d e f g h i j k l m n o p q r s t u v w x y z

数字
0 1 2 3 4 5 6 7 8 9

特殊符号

      • / = , . _ : ; ? \ ” ’ ~ | ! # % & ( ) [ ] { } ^ < > 空格。

关键字
bool true false const enum wchar_t
const_cast dynamic_cast static_cast reinterpret_cast
class public private protected friend inline mutable this virtual
try catch throw
new delete
explicit
namespace using
operator
template typename typeid
union volatile
asm export

标识符
标识符是一个以字母或下划线开头的,由字母、数字、下划线组成的字符串。
标识符不能与任意一个关键字同名。
标识符中的字母区分大小写。
标识符不宜过长。

运算符
单字符组成的运算符:例如,+,-,,/等。
双字符组成的运算符:例如, ++,<=,&&,
=,->等。
三个字符组成的运算符:例如,<<=,>>=等。
关键字运算符:new,delete,sizeof等(这几个运算符平时定义很少有注意)

最短的C++程序
int main() { }

C++中一般用const来代替C语言中的#define
然而我们使用const的时候,我们注意:
声明时一定要赋初值,而且在程序中不能改变其值
Ex:
const int N; (要赋初值)
N = 100; (常量不可以被赋值)
#define与const的区别:

#define
#define PI 3.14
只在预处理期间存在,不占用存储空间
能放在一个头文件里
没有类型信息,可能会隐藏错误

const
const double PI=3.1415;
const double PI(3.1415);
默认为内部连接
可不必创建内存(是否创建内存依赖于对它如何使用)
可放在头文件中,是安全的
extern const int x=0; 强迫为它分配内存
const有两层含义,
用来声明值不可改变的量,等同于C语言中的意义
用来声明可以在编译期确定其值的量,这类似于C语言的#define。
如果一个量的值确实可以在编译期算出来,那么编译器将尽可能对其进行优化,在之后用到此量的地方都直接用其值来代替,从而减少对内存的访问,以达到和#define相同的效率。

C++中的const关键字的用法非常灵活,使用const将大大改善程序的健壮性
const int MONTH=12; //必须给定初值
const 与正常变量一样有作用域
可以节省空间,避免不必要的内存分配
const doulbe Pi=3.14159; //此时并未将Pi放入ROM中
double i=Pi; //此时为Pi分配内存,以后不再分配
double j=Pi; //没有内存分配
编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高

C++中的四种显式转换(强制转换)注意一下

3.函数
函数的参数和返回值的传递方式有:
值传递
指针传递
引用传递:传递的性质象指针传递,使用方式像值传递

引用传递:

引用,是一个变量的别名。

变量名和别名实际上都是指向同一个实体(所以地址相同也当然了)。

&不可以放在void和别名之间。

声明一个别名引用时,必须同时对它进行初始化(这一点和我们的指针很相似),使它指向一个已存在的对象

一旦一个引用初始化后,就不能改成引用其他对象

引用从它诞生之时起,就必须确定是哪个变量的别名,而且始终只能作为这个变量的别名,不能另作他用

引用必须与合法的存储单元关联,不能有NULL引用
引用不分配内存和生成新的变量
不能为常量指定别名
不允许引用数组

使用别名的引用调用在调用函数时,实际上是为实参指定一个别名(函数的形参),在内存中什么也没做

在被调函数内,对形参(别名)的任何操作直接反映到实参

使用引用调用的目的就是使用被调函数来修改主调函数中变量的值

在传值调用时,有变量的两个不同的拷贝
在用别名的引用调用时,只有一份参数变量的拷贝

传递的性质像指针传递,使用方式像值传递

指针“指向”内存中的某个对象,引用“绑定到”内存中的某个对象,它们都实现了对其他对象的间接访问。

区别:
指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以指向不同的对象。引用不是一个对象,无法令引用引用重新绑定到另外一个对象

指针无须在定义时赋初值,如果在块内没有被初始化,将拥有一个不确定的值。引用则必须在定义时赋初值。

内联函数不是在调用时发生控制转移,而是在编译时将函数体嵌入在每一个调用处,节省了参数传递、控制转移等开销。

C++规则指出,inline关键字只是对编译器的请求。根据编译器的效率情况,有时它会忽略这个请求,把函数作为普通函数进行编译。

内联函数代码生成中的一种危险情况是内联长函数,占用更大空间存储多次函数调用所造成的更长的代码

如果内联函数体内出现循环语句或switch,那么执行函数体内代码的时间要比函数调用的开销大

对内联函数不能进行异常接口声明

参数缺省值只能出现在函数的声明中,而不能出现在函数的定义的头部

默认形参值必须从右向左顺序声明。

在默认形参值的右面不能有非默认形参值的参数。

默认参数只能用常量表达式初始化

PS:
Cstdlib
求随机数
RAND_MAX
int rand(void); //求出并返回一个随机数
void srand(unsigned int seed); //为使rand()产生一系列伪随机数而设置起始点。

如果参数是指针,且仅作输入用,则应在类型前加const,以防止该指针在函数体内被意外修改。

如果输入参数以值传递的方式传递对象,则宜改用“const &”方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。

有时候函数原本不需要返回值,但为了增加灵活性,如支持链式表达,可以附加返回值。

如果函数的返回值是一个对象,有些场合用“引用传递”替换“值传递”可以提高效率。而有些场合只能用“值传递”而不能用“引用传递”,否则会出错。

return 语句不可返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。

要搞清楚返回的究竟是“值”、“指针”还是“引用”

如果函数返回值是一个对象,要考虑return 语句的效率。
面向对象程序设计的基本特点
抽象、封装、继承、多态。

类中成员的访问方式:

类中成员互访
直接使用成员名

类外访问
类的数据成员和函数成员具有类作用域,不允许在类外的任何地方被访问
只有在声明了类的对象后,类对象用点访问符(.)访问

关于对象:
类定义是这个类的蓝图,不会分配内存

在创建类的实例(对象)时分配内存

类是一个类型,它的变量是对象
声明形式: 类名 对象名;
例: Clock c1,c2;
DayOfYear today;

对象所占的内存只是用于存放数据成员,函数成员在内存中只占一份复制

不能在类定义时对类的数据成员进行初始化,因为类是抽象的,对象才是具体的。只能通过函数成员进行赋值。

访问权限是相对于类而言,而非对象

可将赋值操作符用于对象
Clock c1,c2;
c1=c2;
但是,当类的数据成员是指针时会有问题

问题:
类定义时分配内存吗?
创建类的对象时分配的空间包括什么?
关于public类的数据成员和成员函数,下面说法对否?
只允许本类对象访问
不允许其他类的对象访问
成员函数参数表中的变量名与类的数据成员名相同时怎么区分?

类与对象(二)
类内数据成员不可以在声明的时候初始化。

数据成员是私有的,程序只能通过成员函数来访问数据成员。

C++要保证一个对象产生的同时必须被初始化

构造函数不可以声明为const和static,但是它的函数体内可以使用。

一般我们不要显式的调用构造函数。

构造函数的attention:
若构造函数带参数,在创建类对象时也带参数
如果类中没有声明构造函数,编译器会自动生成一个默认构造函数,这种情况下,该类的对象的初始化调用此默认构造函数
如果类中声明了构造函数(无论是否有参数),编译器便不会再为之生成默认构造函数
构造函数仅用于初始化数据成员,并进行其他可能需要的任何初始化操作,不要将数据的输入或输出操作放在构造函数中

只有把对象的值传给函数时,才会调用复制构造函数,如果传递引用,则不会调用复制构造函数

如果程序员没有为类声明复制初始化构造函数,则编译器自己生成一个默认复制构造函数
默认复制构造函数实现的只能是浅复制
当类的数据成员是指针类型时,浅复制会带来数据安全方面的隐患
要实现正确的复制,即深复制,必须自己编写复制构造函数

什么是浅复制,什么是深复制。

深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。

浅复制:对象p2=p1的时候,系统采用默认拷贝构造函数(默认的拷贝构造函数不会为新对象重新分配新的内存空间);

深复制:必须自己编写拷贝构造函数,对象p2在拷贝p1的时候获取新的内存空间。

何时调用:
若创建的是自动存储类型对象,在程序执行结束时调用
若创建的是静态存储类型对象,在程序执行结束时调用
若对象是通过new创建的,则驻留在堆栈区,当使用delete释放内存时调用
若创建的是临时对象,则在结束该对象的使用时调用
什么时候程序会调用构造函数?
在创建类的实例(对象)时调用
默认构造函数总会自动生成吗?
只要类中自定义了任何形式的构造函数,编译器就不会再生成默认的构造函数。
什么时候程序会自动调用复制构造函数?
当用类的一个对象去初始化该类的另一个对象时
若函数的形参为类对象,调用函数时,实参赋值给形参
当函数的返回值是类对象时,函数执行完成返回调用者时
何时可以使用类的成员函数
只有声明了类的对象后,才能使用成员函数

缺省的复制构造函数”采用“浅复制”,而非“深复制”的方式来实现,倘若类中含有指针变量,这两个函数注定将出错。

类和对象(三)
当创建类的对象时,如果这个类具有内嵌对象成员(即:类组合),其构造函数设计的原则:
首先自动创建组合类的各个内嵌对象
然后负责对本类中的基本类型成员数据进行初始化

构造函数调用顺序:先调用内嵌对象的构造函数(按内嵌时的声明顺序,先声明者先构造)。然后调用本类的构造函数。(析构函数的调用顺序相反)
若调用默认构造函数(即无形参的),则内嵌对象的初始化也将调用相应的默认构造函数。

必须在构造函数初始化列表中进行的初始化
没有默认构造函数的内嵌对象(这类对象初始化时必须提供参数)
引用类型的数据成员(引用型变量必须在初始化时绑定引用对象)
在这两种情况下,编译器将不为这个类提供默认构造函数,这时必须编写显式的构造函数,并且在每个构造函数的初始化列表中至少为这两类数据成员初始化

当使用前向引用声明时,只能使用被声明的符号,而不能涉及类的任何细节

但是比较特殊的情况是,经过前向引用声明,我们可以声明那个类中的对象指针。

UML中的三个基本部分:事物,关系,图

类和对象(四)
面向过程面向的是算法,而面向对象的面向的是数据结构
在大多数编译器中,空类是可以实例化的
类的每个实例在内存中都有一个独一无二的地址
为了达到这个目的,编译器会给一个空类隐含地加一个字节

Ex: class A{};
Cout<<sizeof(A)

结果为1

4.数据的共享和保护
标识符应先声明,后引用

在同一作用域中,不能声明同名的标识符(重载函数除外)

在没有相互包含关系的不同的作用域中声明的同名标识符,互不影响

如果在两个或多个具有包含关系的作用域中声明了同名标识符,则外层标识符在内层不可见。

如果某个标识符在外层中声明,且在内层中没有同一标识符的声明,则该标识符在内层可见
作用域和可见性的原则不只适用于变量名,也适用于其他各种标识符,包括常量名、用户定义的类型名、函数名、枚举类型的取值等

对象的生存期分为静态生存期和动态生存期
①静态生存期:
生存期与程序的运行期相同
在命名空间作用域中声明的对象
在函数内部声明静态生存期对象
static int i;

②动态生存期:
局部作用域中声明的,没有用static修饰的对象是动态生存期的对象
开始于程序执行到声明点时,结束于命名该标识符的作用域结束处
auto,可省

静态数据成员
用关键字static声明
该类的所有对象维护该成员的同一个拷贝
必须在类外定义和初始化,用(::)来指明所属的类。
静态成员函数
类外代码可以使用类名和作用域操作符来调用静态成员函数。
静态成员函数只能引用属于该类的静态数据成员或静态成员函数。
类的普通数据成员
在声明类的对象时被创建
在类的每个对象中都拥有一个复本
具有类作用域和动态生存期
引用方式:
对象.静态数据成员
对象指针->静态数据成员

类的静态成员
在编译时被创建
所有对象共享一个复本,且与某对象是否存在无关,由该类的所有对象共同维护和使用
静态数据成员是类的成员,而不是对象的成员。
实现了同一类的不同对象之间的数据共享
具有全局的生命周期(静态生存期)
静态数据成员的引用:
类::静态数据成员
对象.静态数据成员
对象指针->静态数据成员

Static是用来声明的

关于static等的访问注意看书上的P157,这里是重点

静态函数成员只能访问同一类中的静态数据成员和静态函数,维护对象之间共享的数据
静态函数成员不能访问非静态数据成员和函数

封装和隐藏是面向对象的两个主要特性
可以保证按所要的方式正确地使用数据成员
但有时需要让外部函数访问私有数据成员

友元有三种
友元函数(普通函数)
友元函数成员
友元类

一、友元函数
一种定义在当前类外部的普通函数
该函数可以访问这个类的私有成员
友元函数不能是自称的友元,需通过friend在当前类内部声明
友元函数不是当前类的函数成员

二、友元函数成员
类可以向其他类的成员提供友元关系,即一个类的友元函数可以是另一个类的函数成员
(这个地方不是很清楚,记得去看)
三、友元类
一个类可以成为另一个类的友元

友元类或友元函数一般通过三个途径访问当前类所有的成员,包括公共属性的、保护属性的和私有属性的成员:
友元函数的入口形参具有当前类的类名
友元函数具有当前类的对象作为局部对象或对象指针作为局部指针。
友元类具有当前类的对象作为其嵌入成员对象

友元关系是不能传递的
友元关系是单向的
友元关系是不被继承的

问题:
程序中为什么要使用静态数据成员?
为什么类的静态函数成员不能访问类的非静态成员?

数据隐藏保证了数据的安全性
数据共享破坏了数据的安全
这个时候我们将急需要声明,也不能被改变的数据声明为常量
所以我们可以用const来进行修饰

常类型的对象必须进行初始化,而且不能被更新

常对象的数据成员被视为常量,通过常对象名访问数据成员不允许被赋值

不能通过常对象调用普通成员函数

用const修饰类成员时
const是函数类型的一个组成部分
在函数声明和实现部分都要带const

关于const限定符的attention:
通过常对象只能调用它的常函数成员,而不能调用其他的函数成员
常成员函数不能更新目的对象的数据成员,也不能调用没有用const修饰的函数成员(保证了在常函数成员中不会更改目的对象的数据成员)
无论是否通过常对象调用常成员函数,在常成员函数调用期间,目的对象都被视为常对象
任何不会修改数据成员的函数都应该声明为const 类型
const可以被用于对重载函数的区分(P164)
构造函数和析构函数都不能是const成员函数

只可以通过构造函数的初始化列表来对常数据成员进行初始化

有关常引用
在声明引用时用const修饰
const 类型说明符 &引用名;
例5-9:常引用做形参(P167)
注意:
常引用所引用的对象不能被更新
非const的引用只能绑定到普通的对象,而不能绑定到常对象
常引用可以绑定到常对象或普通对象
常引用时无论是否绑定到常对象,均将该对象当作常对象,不能修改它的数据成员,不能调用它的非const成员函数

编译预处理命令和条件编译指令都应该看一下

#include 包含指令
将一个源文件嵌入到当前源文件中该点处。
#include<文件名>
按标准方式搜索,文件位于C++系统目录的include子目录下
#include"文件名"
首先在当前目录中搜索,若没有,再按标准方式搜索。
#define 宏定义指令
定义符号常量,很多情况下已被const定义语句取代。
定义带参数宏,已被内联函数取代。
#undef
删除由#define定义的宏,使之不再起作用。

5.数组、指针与字符串

注意常量指针和指针常量
Const int *p=&a a不可以更改,但是地址可以
Int *const p=&a p的地址不可以
主要是看const在哪里

void指针
任何类型的指针都可以赋值给void类型的指针变量
void类型指针可以存储任何类型的对象地址
经过强制类型转换,通过void类型的指针便可以访问任何类型的数据

指针型函数和指向函数的指针要看一看概念

函数指针:专门用来存入函数代码首地址的变量
一旦函数指针指向了某个函数,即可使用函数指针来调用函数
声明:数据类型 (*函数指针名)(形参表);
如:void (*func) (float a);
函数指针在使用之前必须赋值:
函数指针名=函数名;
调用:函数指针名(形参);
注意:该指针只能指向已经声明,且与该函数指针具有相同返回值类型和相同形参表的函数

类名 *对象指针名;
Point A(5,10);
Piont *ptr;
ptr=&A;
通过指针访问对象成员
ptr->getX()
相当于 (*ptr).getX();

This指针
隐含于每一个类的成员函数中的特殊指针。
明确地指出了成员函数当前所操作的数据所属的对象。
当通过一个对象调用成员函数时,系统先将该对象的地址赋给this指针,然后调用成员函数,成员函数对对象的数据成员进行操作时,就隐含使用了this指针。

this指针是一个指针常量,对于常成员函数,this同时又是一个指向常量的指针
在一般的程序设计中,通常不直接使用this指针来引用对象成员
主要在两种情形中使用
多个变量同名时,使用this指针澄清所指的是哪个变量
this->X=X;
要返回传送到函数的对象时
return *this;
*this用来标识正在调用该函数的对象

This可以静态么

P125注意看

类型说明符 (类名::*指针名)(形参表);
如:int (Point::*p_GetX) ( )
指针名=&类名::成员函数名;
如:p_GetX=&Point::GetX
通过对象名(或对象指针)与成员指针结合来访问成员函数
(对象名.*类成员指针名)(参数表)
如:(A.p_GetX)()
(对象指针名
类成员指针名)(参数表)
如:(p1->GetX)()

指向类的静态成员的指针
对类的静态成员的访问不依赖于对象
可以用普通的指针来指向和访问静态成员
通过指针访问类的静态数据成员
通过函数指针访问类的静态成员函数

左边的变量应与右边的变量类型匹配
new运算符无法从自由空间分配存储时(即没有满足请求的内存容量时),会抛出异常

int *pn=new int(); //初始化为0

情况1:该类存在用户定义的默认构造函数(自定义默认构造函数),则new T和new T()都会调用这个默认构造函数。
情况2:用户未自定义的默认构造函数(系统自动产生),则new T调用这个默认构造函数,而new T()除了调用这个默认构造函数外还会用0赋初值。

用new申请的内存必须用delete释放(一对一)

若被删除的是对象,则调用该对象的析构函数
一定要配对地使用new和delete,否则将发生“内存泄漏”
对于用new建立的对象,只能使用delete进行一次删除操作,否则将导致运行错误

int *p1=new int[10]; //没有初始化
int *p2=new int10;//初始化为0

是否加()效果与new T和new T()一个道理,不同的是()内不可以有值

如果使用new为一个实体分配内存,则应使用delete来释放
如果使用new[]为数组分配内存,则应使用delete[]来释放

vector是一种随机访问的被封装的动态数组类型
vector是一个类模板

vector数组对象的名字表示的是一个数组对象,而不是数组的首地址
数组对象不是数组,而是封装了数组的对象

浅复制
默认的复制构造函数
实现对象间数据元素的一一对应复制

深复制(特别是指针)
如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针

深复制的核心思路是:
目标对象与源对象内存空间独立,相应指针成员指向的内存空间也彼此独立
全部复制源对象的数据到目标对象,包括分别复制指针成员指向的内存数据
必须定义一个复制构造函数,通过深复制将一个对象初始化为另一个对象
复制构造函数应分配足够的空间来存储复制的数据,并复制数据,而不仅仅是数据的地址。另外还应更新所有受影响的静态类成员。

继承与派生1

实现代码重用,减少了代码的冗余
能使代码易于维护
基类的任何变动自动改变了子类的行为

派生其他类之后,基类仍然保持不变。
继承是单向的,即基类对派生类一无所知。
派生类只有在基类功能之外增加功能,而不能减少从基类继承的函数和成员变量。
派生类不能直接访问基类的私有成员,而必须通过基类的公有方法进行访问,即派生类构造函数必须使用基类构造函数。
继承关系是可以传递的
派生类继承了基类全部数据成员和除构造、析构函数之外的全部成员函数,但是这些成员的访问属性在派生的过程中是可以通过继承方式调整的

记住不管以哪种方式继承,私有数据都没有被派生类实际拥有,被隐藏了,更不是派生类的私有成员。

继承与派生2

类型兼容:指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代,为什么?

一个公有派生类的对象在使用上可以被当作基类的对象,反之则禁止
替代之后派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员

通过公有继承,派生类得到了基类中除构造函数、析构函数之外的所有成员。公有派生类具有基类的所有功能。

派生类对象可以隐含转换为基类对象
派生类对象可以赋值给基类对象
base=derived;
派生类对象可以初始化基类的引用
Base &rbase=derived;
派生类的指针可以隐含转换为基类的指针
派生类的地址可以赋给指向基类的指针
Base *pb;
pb=&derived;

注意:通过基类对象名、指针只能使用从基类继承的成员(仅发挥出基类的作用)
使用多态可以保证基类、派生类各自发挥出各自的特性
类型兼容规则是多态性的重要基础之一

构造函数不被继承,派生类必须自行声明

析构函数不被继承,派生类必须自行声明

当使用派生类建立一个派生类对象时,将首先产生一个基类对象,依附于派生类对象中
构造派生类对象时,就要对基类数据成员、新增数据成员和成员对象进行初始化
在派生类对象的成员中,从基类继承来的成员被封闭为基类子对象,它们的初始化由派生类构造函数隐含调用基类构造函数进行初始化
内嵌成员对象则隐含调用成员类的构造函数进行初始化
派生类新增的数据成员由派生类在自己定义的构造函数时进行初始化

调用次序:先基类,再派生类
第一步:调用基类构造函数,调用顺序按照它们被继承时声明的顺序(从左向右)
第二步:调用内嵌成员对象的构造函数,调用顺序按照它们在类中声明的顺序
第三步:本派生类的构造函数体中的内容

如果没有编写复制构造函数,编译系统自动生成一个默认的复制构造函数
若建立派生对象时调用默认复制构造函数,编译系统自动调用基类的复制构造函数
如果要为派生类编写复制构造函数,则需要为基类相应的复制构造函数传递参数

指针数组和数组指针例如:A (*p)[3]和A *p[3]都不调用构造函数。

继承与派生3

如果派生类的函数与基类的函数同名,但是参数不同。那么,无论有无virtual关键字,基类的函数将被隐藏。
如果派生类的函数与基类的函数同名,且参数形同,。但是基类的函数前没有virtual关键字。此时,基类函数将被隐藏。

如果某个派生类的多个基类拥有同名的成员
(两种情况)
情况一:若派生类又新增这样的同名成员,在这种情况下,派生类成员将隐藏所有基类的同名成员
唯一标识派生类新增同名成员
对象名.成员名
对象指针->成员名
唯一标识基类同名成员
对象名.基类名::成员名

P273 using 也有它的两个新功能
情况一:将基类的作用域引入另一个作用域
情况二:using用于基类中的函数名,且如果派生类中定了同名但参数不同的函数。(等价于函数的重载)

作用域分辩符和虚基类的区别
作用域分辩符:
在派生类中拥有同名成员的多个副本
通过直接基类名获得唯一标识
可以存放不同的数据、进行不同的操作
虚基类:
在派生类中拥有同名成员的一个副本
通过成员名即可唯一标识
节省内存空间

建立一个对象时,如果这个对象含有从虚基类继承的成员
则虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化
只有最远派生类的构造函数会调用虚基类的构造函数,该派生类的其他基类对虚基类的构造函数的调用都自动被忽略
最远派生类的构造函数的职责是对虚基类进行初始化。这意味着该类不管离虚基类多远,都有责任对虚基类进行初始化。

多重继承时构造函数的调用次序
虚基类构造函数
非虚基类构造函数,按继承的次序
内嵌对象构造函数,按声明的次序
派生类构造函数函数体

若虚基类由非虚基类派生而来,则仍然先调用基类构造函数,再调用派生类构造函数。(因为先有基类才有派生类)

多态性

多态性是面向对象程序设计的重要特征之一。
多态性是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为。

多态的实现:
函数重载
运算符重载
虚函数

专用多态
重载多态:函数及运算符的重载
强制多态:强制类型转换

通用多态
包含多态:类族中定义于不同类中的同名成员函数的多态行为,主要通过虚函数来实现
参数多态:与类模板相关,在使用时必须赋予实际的类型才可以实例化

编译时的多态(静态多态):在编译过程中确定了同名操作的具体对象
重载多态、强制多态、参数多态

运行时的多态(动态多态):在程序运行过程中才动态地确定操作所针对的具体对象
包含多态
这个过程称为绑定

静态绑定:
在编译阶段完成(早期绑定或前绑定)
在编译、连接过程中,系统可以根据类型匹配等特征确定程序中操作调用与执行该操作代码的关系
重载、强制和参数多态
动态绑定
在程序运行阶段完成(晚期绑定或后绑定)
在编译、连接过程中无法解决的绑定问题,要等到程序开始运行之后再来确定
包含多态

. .* ->* :: sizeof ?: 这些运算符不可以重载

只能重载C++语言中已有的运算符,不可臆造新的。
不改变原运算符的优先级和结合性。
不能改变操作数个数。
经重载的运算符,其操作数中至少应该有一个是自定义类型。

重载为类的成员函数
重载为非成员函数(可以是友元函数)

重载为类成员函数时
参数个数=原操作数个数-1 (后置++、–除外)
重载为非成员函数时 参数个数=原操作数个数,且至少应该有一个自定义类型的形参。

对于对象的引用返回,引用是已经建立的对象的别名,返回的是不独立的对象,此时编译器不需要额外建立临时对象。
对于对象的数值返回,返回一个局部或临时的独立对象,输送给主控程序。主控程序会尽早释放临时对象占有的内存。
算术和指针类型的数值返回其函数调用为右值表达式,对象类型的数值返回其函数调用可以为左值,但由于返回的临时对象的生存期不由程序员控制,因此返回数值对象的函数调用不宜作为左值参与运算。
返回引用的函数调用则可作为左值参与运算。

Attention:
我们在对一个运算符重载的时候,我们很多时候我们都会在前面加上一个&,主要是为了效率和节省空间,还有一个原因就是因为有的运算符操作的是一个左值,而且修改的是自己原来的对象,所以用引用来修改实体,而不是创建的临时对象,参数用引用也是同样的道理,如果用的不是引用,在这个函数运算完了之后,不仅会删除这个临时对象,它的相关地址也会删了,没有操作到真正的对象本身,不仅没有用,而且还有可能会导致内存泄漏。

多态性2

C++中的大多数操作符都可以通过成员或非成员函数进行重载,但是
下面的操作符只能通过成员函数进行重载:

=
()
[]
-> 通过指针访问类成员的操作符
下面的操作符只能通过非成员函数进行重载
<< //插入运算符 >> //提取运算符

对于任意一个类A,如果编写函数,C++编译器将自动为A 产生四个缺省的函数
A(void); // 缺省的默认构造函数
A(const A &a); // 缺省的拷贝构造函数
~A(void); // 缺省的析构函数
A& operator =(const A &a);
// 缺省的赋值运算符函数

若自己没有编写“=”重载函数,则系统提供默认的“=”重载函数
系统提供的默认的“=”重载函数是浅拷贝,若类中的数据成员是指针,会造成内存泄漏,内存重复析构。所以,数据成员有指针时,需要自己编写“=”重载函数以实现深复制
若自己编写“=”重载函数,则系统不再提供默认的“=”重载函数(注意:只有程序显式提供了以本类或本类的引用为参数的赋值运算符重载函数时,编译器才不会提供默认的版本:例CH8-2-3.CPP)
警告:只要类中的数据成员动态分配了内存,就应该编写析构函数、拷贝构造函数和赋值运算符(Big three)

管理对象动态分配内存的“三大”例程:析构函数、拷贝构造函数、赋值运算符。只要编写了其中一个,就必须编写其他函数。
拷贝构造函数是在对象被创建时调用的
opeator=函数只能被已经存在了的对象赋值时调用

当类中有指针属性成员时,赋值函数一般分四个步骤:
检查自赋值;
释放原有内存资源;
分配新的内存资源,并复制内容;
返回 *this
MyStr& operator =(const MyStr& str)
{
cout << “operator =” << endl;
if (this != &str) // (1) 检查自赋值
{
if (name != NULL)
delete[] name; // (2) 释放原有的内存资源
int len = strlen(str.name);
name = new char[len + 1]; //(3)分配新的内存资源
strcpy(name, str.name); //(3)复制内容
this->id = str.id;
}
return *this; // (4)返回 *this
}
可见:当用一个非类A的值(如int型值)为类A的对象赋值时:
①如果匹配的构造函数和赋值运算符重载函数同时存在,会调用赋值运算符重载函数
②如果只有匹配的构造函数存在,就会调用这个构造函数

赋值运算符重载函数不能被继承

当类中的属性有指针成员时
必须使用深拷贝,并且所有的构造函数采用同样的方式构造(要么都是new要么都是new[])
必须编写析构函数(delete或delete[]

【】和()两种特殊运算符的重载

一旦编写了operator+,不仅可以将两个类对象相加,还可以将该类对象与int、double相加。因为编译器不仅会试着查找合适的operator+,还试图查找合适的类型转换,构造函数会对有问题的类型进行适当的转换。

当c2 = a + 2;时,会调用构造函数构建一个临时的Complex对象(值为2),再传递给operator+()

多态性3

虚函数声明为内联函数不会出现错误但是就成了静态处理了

虚函数是动态绑定的基础。

是非静态的成员函数,虚函数一般不声明为内联函数
virtual 只用来说明类声明中的原型,不能用在函数实现时。
具有继承性,基类中声明了虚函数,派生类中无论是否说明,同原型函数都自动为虚函数。
调用方式:通过基类指针或引用,执行时会
根据指针指向的对象的类,决定调用哪个函数。

多态的条件(3个)
派生类中覆盖(override)了基类的虚函数(类之间满足赋值兼容规则)
基类中有虚函数
通过成员函数来调用或者通过指针(或引用)访问虚函数

将派生类对象赋值给基类的指针(或引用)通过基类指针(或引用)调用虚函数根据传递的派生类的指针(或引用)的不同实现访问的多态。

用指向派生类对象的指针仍然可以调用基类中被派生类覆盖的成员函数(如:Base1是基类,ptr是指向派生类的指针)
使用“::”限定
ptr->Base1::display()//仍指向的是基类函数

我们一般习惯将基类中的析构函数设置为虚函数:
可以解决下面的问题:
Base *b=new Derived(); //开辟一个Derived类的对象(含指针p)并赋给基类指针
fun(b); //将基类指针传递给fun函数,在函数中删除指针b,但是Derived中开辟p指向的内存未被释放

如果有可能通过基类指针调用对象的析构函数(通过delete),就需要让基类的析构函数成为虚函数,否则会产生不确定的后果。

可以创建指向抽象类的指针和引用,所以抽象类能支持动态多态
注意
抽象类只能作为基类来使用。
不能声明抽象类的对象。
构造函数不能是虚函数,析构函数可以是虚函数

多态性可大大简化系统的界面,使得不同的但又具有某种共同属性的对象不但能在一定程度上共享代码,而且还能共享接口
提高了系统的一致性、灵活性和可维护性

  • 6
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值