C++语言学习
C++学习笔记(wust学习通)
一.C++简单程序设计
1)程序设计语言的发展
1.发展:机器语言 汇编语言 高级语言{面向过程、面向对象}
2.高级语言早期是面向过程
3.20世纪60年代产生了结构化的程序设计思想:采用了模块分解与功能抽象以及自顶向下,分而治之的方法,从而有效的将一个较复杂的程序系统设计任务,分解为许多易于控制和处理的子任务,便于开发和维护。(面向过程)
它把数据和处理数据的过程分为独立的实体,导致当数据结构改变时,所有相关的处理过程都要进行相应的修改**(程序可重用性差)**、并且由于图形用户界面的使用,应用软件应随时响应用户的可能操作,因此软件的功能,很难用过程来描述和实现。
2)面向对象的语言
1.出发点:能更加直接地描述客观世界。
2.客观世界可以分类:对象是类的实例,对象是数据和方法的封装,对象间通过发送和接受消息发生联系。
(类的继承与多态性可以提供使用现成类的机制,从而实现代码的重用)
3)对象
现实世界中一个存在的事物
**面向对象方法中的对象:**是系统中用来描述客观事物的一个实体。用来构成系统的一个基本单位,对象由一组属性和一组行为构成。
二.函数
1.函数的参数传递
1)引用:
引用是已存在变量名的别名,对引用型变量的操作实际上就是对被引用变量的操作。当定义一个引用型变量时,需要用已存在的变量对其初始化。
语法格式:数据类型 **&**引用变量名 = 变量名;
注意:
1>声明一个引用必须同时对其初始化,使它指向一个已经存在的变量。一旦一个引用被初始化,就不能改为指向其他变量。
2>定义一个引用变量后,系统并没有为他分配内存空间。
3>refx与被引用变量x具有相同的地址,即refx与x使用的是同一内存空间。
4>对引用变量值的修改就是对被引用变量的修改,反之亦然。
2)传值调用
值传递:
· 将参数值传给形参;
· 实参和形参占用各自的内存单元,互不干扰,函数中对形参值的改变不会改变实参的值,属于单向数据传递方式。
传址调用:
· 以指针作为函数的形参,在调用时将实参传递给形参,实参和形参指针变量指向同一内存地址。
· 通过形参指针对数据值的改变,同样影响着实参所指向的数据值,从而实现参数双向传递。
引用调用:
swap函数的实现:
#include<iostream>
using namespace std;
void swap(int &x,int &y)
{
int t;
t=x;
x=y;
y=t;
}
int main()
{
int a=5,b=9;
cout<<a<<";"<<b<<endl;
swap(a,b);
cout<<a<<";"<<b<<endl;
return 0;
}
优点:函数体实现比指针简单;调用函数语法简单;对于复杂类型效率较高。
2.内联函数
· 通过在编译时将函数体代码插入到函数调用处,将调用函数的方式改为顺序执行方式来节省程序执行的时间开销,这一过程叫做内联函数的扩展。即用空间换时间。
定义格式:
inline 函数类型 函数名 (形式参数表)
{
函数体;
}
在内联函数扩展时也进行了实参与形参结合的过程:
先将实参名,将函数体中的形参处处替换,然后搬到调用。但在用户视角,调用内联函数和一般函数没有任何区别。
优点:
1>把功能相对独立的代码封装成函数,便于阅读和理解;
2>使用函数可以确保行为的统一,每次相关操作都能保证按照同样的方式进行;
3>便于对计算过程进行修改;
4>便于重复使用;
缺点:
1>调用函数一般比求等价表达式的值要慢一点;
2>可能需要拷贝实参;程序转向一个新的位置继续执行;
一般使用在相对简单且独立,需要被频繁调用的使用场景,如求面积;
3.带默认形参值的函数
1.C++允许在函数说明或函数定义中为形参预赋一个默认的值,这样的函数就叫做带有默认形参值的函数。
2.在调用带有形参值的函数时,若为相应形参指定了实参,则形参将使用实参的值,否则形参将使用其默认值。
例子:
int sub(int x=8,y=3)
{
return x-y;
}
int main(){
sub(20,15);//20,15
sub(10);//10,3
sub();//8,3
return 0;
}
注意:
1.函数有多个形参,默认形参必须从右到左连续定义,并且在一个默认形参值右边不能有未指定默认值的参数(由于c++函数调用是从左到右初始化)。
2.调用函数时,如果省去了某个实参,则直到最右端的实参都要省去(它们对应的实参都要有缺省值)。
3,如果存在函数原型,形参值应在函数原型指定,否则在函数定义中指定。
4。函数原型给出了默认值,函数定义中不得重复指定,即使所指定的缺省值完全相同也不行。
5.在同一作用域,一旦定义了默认形参值,就不能再定义它。如果几个函数说明出现在不同作用域,则允许分别为他=它们提供不同默认形参值。
6.对形参默认值的指定可以是初始化表达式,可以包含函数调用。
7.在函数原型给出了形参的默认值时,形参名可以省略。
4.函数重载
概念:俩个以上的函数,取相同的和函数名,但是形参的个数和类型不同,编译器根据实参和形参的类型及个数的最佳匹配,自动决定调用哪个函数。
例:
// 计算两个整数之和
int add(int a, int b) {
return a + b;
}
// 计算两个浮点数之和
float add(float a, float b) {
return a + b;
}
// 计算三个整数之和
int add(int a, int b, int c) {
return a + b + c;
}
注意:
1.各个重载函数返回值可以相同,也可以不同。但不能仅仅返回值不同,编译时会认为是语法错误。
2.不要把功能不同的函数定义为重载函数,以免对调用结果的误解和混淆。
3.确定对重载函数的哪个函数进行调用的过程称为绑定。绑定优先级依次为:精确匹配、对实参类型向高转换后的匹配、实参类型向低类型及相容类型转换后的匹配。
绑定(匹配)二义性:
俩个重载函数,编译器不知道进行那种类型转换,与哪个函数绑定。
消除二义性方法:
1.添加重载函数定义;
2.将函数实参进行强制类型转换,使调用精确匹配。
重载函数和带默认值的函数一起使用也可能引起二义。
三.类和对象
1.面向对象程序设计的基本特点
抽象 封装 继承与派生 多态
1)抽象
对对象概括,抽出公共性质并描述。
具体可分为(以时钟类举例):
数据抽象:描述属性或状态。(int hour,int minute,int second)
行为抽象:描述共同行为或功能特征。(SetTime(),ShowTime())
2)封装
将抽象的数据和行为结合,即将数据与操作数据的代码进行有机的结合形成类。
目的:
1.合理控制访问权限,增强了数据安全性、简化程序编写工作。
2.复用性好,可以直接使用,不必了解其中内容。
3)继承与派生
把已有类属性、行为继承过来并且加上新的属性和行为,形成新的类。
4)多态性
指同一个名字,可以有不同的功能实现方法。
目的:达到行为标识统一,减少程序中标识符的个数。
2.类和对象
类的定义:
类是一个包含函数的结构体,类的定义与结构类型相似
格式:
class 类名
{
public:
公有数据或公有函数成员的定义;
protected:
保护数据或保护函数成员的定义;
private:
私有数据或私有函数成员的定义;
}
说明:
1.class表明定义的是一个类,类名要求是一个合法的标识符
2.类的成员有数据成员与函数成员,数据成员用来描述该类对象的属性,称为属性,函数成员是描述类的行为,称为方法,也叫做成员函数。
3.pubilc、protected、private分别表示对成员的不同访问权限控制,如果前面没有标明访问权限,默认访问权限为private。
数据成员:
描述了类对象所包含的数据类型,数据成员的类型可以是C++基本数据类型,也可以是构造数据类型或定义完整的类类型。
成员函数:
可以在类内声明,类外定义。相当于在类内列出了一个函数功能表。
格式:
返回值类型 类名::成员函数名(形参表)
{
函数体;
}
**:: ** 类的作用域分辨符,放到类名后成员函数前,表明后面的成员函数属于前面的那个类。
隐式声明:直接在类内部定义函数
对象的建立与使用:对象名.属性 对象名.成员函数名(实参1,实参2,…,)
成员的存取控制:
存取属性 | 意义 | 可存取对象 |
---|---|---|
public | 公开(公有)级 | 该类成员及所有对象,任何外部函数都可以访问 |
protected | 保护级 | 该类及其子类成员 |
private | 私有级 | 该类成员,只能被本类成员函数访问 |
3.构造函数和析构函数
· 构造函数:
类和对象的关系就相当于基本数据类型和它的变量之间的关系;
每个变量在程序运行时都要占据一定的内存空间,如果在定义的时候进行了初始化,则会在给变量分配内存单元的同时,写入变量的初始值;
编译器会根据变量类型自动生成一些代码,来完成初始化过程;
为什么需要构造函数:
当遇到对象声明语句时,程序会向操作系统申请一定的内存空间用于存放新建对象,与普通变量相比,类的对象太复杂,编译器不知如何让产生代码来完成初始化,因此对象的初始化就需要程序员来自己编写。
构造函数的作用:
在变量被创建的时候用特定的值构造对象,或者将对象初始化一个特定的状态。
在对象创建时由系统自动调用。
构造函数一些特殊的性质:
1.构造函数的函数名与类名相同,而且没有返回值
2.构造函数通常被声明为公有函数
3.只要类中有了构造函数,编译器就会在建立新对象的地方插入对构造函数调用的代码
调用时不需要提供参数的构造函数,称为默认构造函数:
class Clock
{
public:
Clock(){}//编译系统生成的隐函的默认构造函数
...
}
注意:它产生的值是随机的
如果类中声明自定义构造函数,则不会产生默认的:
(此处展示部分代码)
Clock(int newH=0,int newM=0,int newS=0)//带默认形参
{
H=newH;
M=newM;
S=newS;
}
int main()
{
Clock myClock(0,0,0);
myClock.showTime();
return 0;
}
如果声明了却没有定义则会出错,所以一般情况下,给类定义了一个带参数的构造函数,还会定义一个默认构造函数,就是不带参数的构造函数.
构造函数和setTime()作用相通;
· 析构函数:
构造函数动态申请的一些内存单元,在对象消失前要释放这些内存单元,这时需要析构函数完成这类工作.
析构函数特点:
1.通常是类的公有成员函数;
2.析构函数的名称是类名前面加上波浪线;
3.析构函数没有返回值;
4.与构造函数不同的是,析构函数不接受任何参数,但是可以是虚函数;
5.一般用户自定义,对象消失时系统自动调用;
6.若不显式说明,系统会生成一个函数体为空的隐含析构函数;
顺序构造,逆序析构
4.复制构造函数
基本类型变量:可用已经定义好的变量初始化
定义对象时:用已经存在的对象初始化新对象
自定义类型:编译器不知道怎么做
复制构造函数可以规定如何用一个已经存在的对象去初始化同类的另外一个对象。
模板如下:
class 类名
{
public:
类名(形参);//构造函数
类名(const 类名 &对象名)//复制构造函数的实现
...
}
类名::类名(const 类名 &对象名)//复制构造函数的实现
{函数体}
是一种特殊的构造函数。其形参为本类的对象引用。
由于传递引用作为参数时,可以实现数据的双向传递,如果在函数中对形参修改,实参也会同步修改,我们希望原有对象不被修改,加上const进行限定;
在类外实现复制构造要加上**类名(凸显)**进行限制;
何时调用复制构造函数:
1.当用类的一个对象初始化该类的另一个对象;
2.函数形参为类对象,调用函数时实参赋值给形参;
(只有把对象用值传递时,才会调用复制构造函数,传递引用则不会调用)
3.当函数返回值是类对象;(编译器先创建一个临时无名对象那个,执行语句return A时,调用复制构造函数,将对A的值复制到临时对象)
5.程序实例
游泳池,栅栏求面积
1.面向对象:Circle类,见example1.cpp
2.面向过程
6.类的组合
1)组合类的构造函数
描述一个类内嵌其他类的对象作为成员,它们之间的关系是一种包含与被包含的关系。
例子:Line类内嵌Point类;
1.首先我们知道每个类的构造函数只能完成本类成员的成员初始化;
2.其次一个内的私有成员只能这个类的内部成员函数可以访问,类外不能访问;
如何完成组合类的初始化?
使用初始化列表对内嵌对象初始化
组合类的构造函数的语法定义形式:
类名::类名(形参表):内嵌对象1(参数),内嵌对象2(参数)...
{本类初始化}
构造函数的调用顺序:
先调用内嵌对象的构造函数,内嵌对象先定义先构造。
注意:
1,内嵌对象在构造函数初始化列表出现顺序与内嵌对象构造函数的调用顺序无关;
2,整个初始化列表执行完成后,再执行组合类构造函数的函数体;
3,析构与构造正好相反;
2)组合类的复制构造函数
注意本类数据在函数体初始化;类外对象在初始化列表初始化
本节过后设计一个点线组合类要求能求长度,见example2.cpp
四.数据的共享与保护
1.作用域和生存期
1)标识符的作用域
· 一个标识符在程序正文中有效的区域
· 标识符的有效范围
包含:
函数原型作用域、局部作用域(块)、类作用域、命名空间作用域;
函数原型作用域:
在函数原型声明时形式参数的作用范围
double Area(double radius);
radius的作用域在()之间,可省去,但为了可读性,通常给出形参名;
局部作用域:
限于块中,例如函数块
类作用域:
如私有成员只有成员函数访问
命名空间作用域:
全局作用域的划分,大型项目防止命名冲突;
语法:
namespace命名空间名
{命名空间的各种声明(函数声明,类声明,...}
使用using:
1.using MYNS::Clock; Clock d2;
2.using namespace MYNS;
把命名空间中所有标识符暴露在当前作用域内;Clock d2;
3.using namespace std;包含所有的标识符,但这是不好的,因为它把一切都暴露在了当前的命名空间内;
俩类特殊命名空间:
全局命名空间:默认
匿名命名空间:
需要显示说明但没有名字;
多文件编程中,常用于屏蔽不希望暴露给其他源文件的标识符;
2)标识符的可见性
由内向外:块、类、命名空间
声明在前,引用在后;
不可定义同名标识符(重载除外);
内可用外,外不可用内;
没有包含关系的不同作用域中声明的同名标识符,互不影响;
3)对象的生存期
静态、动态
静态:与程序运行期相同;
动态:诞生于声明的点、结束与该标识符的作用域结束处;
2.类的静态成员
实现既共享又隐藏
1.静态数据成员
声明:
以员工系统求员工总数举例:
static int count;//静态数据成员,类作用域,设置为类内私有数据
int Empolyee::count = 0;
类型 类名 分配空间并初始化
为何private的count可以在类外直接赋初始值?
c++规定,static数据成员初始值不受任何存取权限束缚。
2.静态成员函数
定义格式:
static 返回值 成员函数名(参数表);
引用格式:
1.对象名.
2.类名::
管理静态数据成员,完成对静态数据成员的封装;
不依赖任何对象,可以在main函数中通过类名调用;
只能访问该类静态数据成员;
若要访问非静态数据成员,须通过参数传递对象名,然后通过对象名访问非静态数据成员;
3.类的友元
主动声明拿些其他类或函数是它的朋友,进而给他们提供对本类的访问特许权;
1.友元函数
用friend修饰的非成员函数;
是只用于声明的关键字;
2.友元成员函数
一个类的友元函数可以是另一个类的成员函数,这时要将原类的作用域加上;
注意是否要先项引用说明;
3.友元类
当希望一个类中所有成员函数,都可以存取另一个类的私有数据成员时
friend class 类名;
若B为A的友元类:
1.B类所有成员函数自动成为A类友元函数;
2.B类所有成员函数中都能直接访问A类的私有成员和保护成员;
4.共享数据的保护
数据隐藏保证了数据安全性;
各种形式的数据共享(如友元)破坏了数据的安全性;
对策:
将既需要共享,有需要防止改变的数据声明为常量;
用const关键字进行修饰;
const:可定义常量,修饰函数参数,返回值,定义体;
常类型:
1)常对象
声明为const的对象,其数据成员不可被改变;
2)常成员函数
声明:类型 函数名(参数表)const;
const放在后面;
注意:
1.const是函数类型的组成部分,因此在函数声明和实现部分都要带上const;
2.通过常对象只能调用它的常成员函数,而不能调用非常成员函数,这也是对常对象的保护;
3.常对象和非常对象能调用常成员函数;
const成员函数的意义:
承诺在本函数内部不改变类内数据成员,为此,也只能调用承诺不改变成员的const成员函数,保证了在常成员函数中不会更改目的对象的数据成员;
3)常数据成员
对常数据成员的初始化,只能通过构造函数的初始化列表进行;
4)常引用
声明引用时用const修饰,则被声明的引用就是常引用.
const 类型说明符 &引用名;
注意:
常饮用所引用的对象不能被更新;
非const的引用只能绑定到普通的对象,而不能绑定到常对象.而const引用可以绑定到常对象或普通对象;
5.多文件结构和编译预处理命令
注意:
类的定义必须出现在所有使用该类的编译单元中;
将类的定义写在头文件中,使用该类的编译单元则包含这个头文件;
通常划分:
类定义文件(.h)
类实现文件(.cpp)
类的使用文件(.cpp,主函数)
#include使用:
自带用<>,自己编写用""
外部变量:
在定义源文件中使用;
被其他文件使用;
命名空间作用域中声明的变量默认为外部变量,但在其他源文件中使用时要加extern加以声明;
外部函数:
1.都是非成员函数;
2.都具有命名空间;
3,没有特殊说明,可以在不同编译单元中被调用,只要在调用前引用性声明;
将变量和函数限制在编译单元内:
命名空间作用域中声明的变量和函数,默认情况下都可以被其他编译单元访问;
若不想:
1.static修饰变量和函数名;
2.匿名的命名空间;
编译预处理:
编译前,预处理器对程序文本预处理;
#include指令;
#defined和undef:
#define PI 3.14
#define HEAD_H
#undef删除define定义的,使之不再有作用;
条件编译:
1.#if 常量表达式
程序段
#endif//为真则执行
2.#if
#else
#endif
3.#if常量表达式1
#elif常量表达式2
#else常量表达式3
#endif
4.#ifdef 标识符//标识符没删除就执行
#else
#endif
5.#ifndef 标识符//与4相反
#else
#endif
通过编译预处理可以实现避免重复定义;
#ifndef HEAD_H
#define HEAD_H
五.数组指针和字符串
1.对象数组和对象指针
1)对象数组
数组的类型是自定义类型,对象数组的元素是自定义类
声明方式:
类名 数组名[常量表达式];
Point pt[10];
访问方法:下标
数组名[下标表达式].成员名
pt[1].print();
对象数组初始化:
每个元素对象被创建时,都会自动调用该类的构造函数
2)对象指针
用于存放对象地址的变量
声明形式:
类名 *对象指针名;
Point *ptr;
ptr=&A;
通过指针访问对象成员:
ptr->getX();
等价于:
(*ptr).getX();
注意:对象指针使用前先初始化
3)this指针
类对象维护自己的状态变量,同一个类的对象只有一个成员函数的拷贝
成员函数被调用时如何访问对象变量:
this指针
隐含于每个类的非静态成员函数中,包括构造函数和析构函数;
this指针指向正在被成员函数操作的对象,是一个指针常量;
每次调用成员函数时,对象的this指针作为隐藏成员传递给成员函数;
对于常成员函数,this是一个指向常量的指针常量:
constX *const
主要有俩种this指针引用对象成员的情况:
1)变量同名,用this指针澄清所指的变量:
void Point::Move(int x,int y)
{
this->x=x;
this->y=y;
}
2)要返回传送到函数的对象时:
return *this;
标识正在调用该函数的目的对象;
4)指向类的静态非公有数据成员和函数的指针
声明方式:
*变量名=某个数据成员;
返回值 (*函数名)()=某个函数;
2.动态内存分配
程序在运行时得到内存
必须显式的释放;
申请和释放的内存空间:堆
new:
类型 指针变量= new 类型(初始化列表);
*int pn2=new int(2);
创建动态数组:
int *p1=new int[5]()
int *p1=new int[5]{1,2,3,4,5}
动态分配一个数组,会返回一个元素类型的指针;
释放动态数组:
delete []p1;逆序销毁数组中元素,并释放该内存;
注意:
等号左边变量的类型应与右边变量类型相匹配;
若创建类对象,则调用该类对象构造函数;
new,delete配对,避免内存泄露、重复释放;
delete只能释放new创建的;
不能用sizeof;
3.深复制和浅复制
1.类中仅含有简单变量或对象成员,没有指针数据成员:
1)系统提供默认的复制构造函数和赋值运算符重载函数都是浅复制;
2)该方式通过memcpy函数将远视力的数据复制到目标实例占有的一片内存空间;
3)默认的浅复制方式时安全的;
2.类中含有指针成员:
1)数据存放在堆上,如果浅复制,则仅仅是指针的复制;
2)一个空间被多个指针指向,多次析构,说明浅复制不再胜任;
默认的复制构造函数是简单的等位拷贝,也就是俩个指针指向同一空间实现的值相等;
深复制:
自己编写复制构造函数,为目的对象开辟新的空间;
还要重载赋值运算符,实现对象间的赋值;
4.string类
含头文件
构造string类对象:
string s1;
string s2(“friday”);
string s3(s2,0,3)//s2从第0个字符开始三个字符;
string s4(“friday”,3)/前三个字符构造;
string s5(10,‘A’)//一个字符重复n次;
读写string类对象:
io运算符;
getline(cin,str)//ctrl+z;\n;达到最大输入长度;
c++重载了string类的运算符,使之类同于普通数据可以进行一系列操作,size()求长度;
比较规则:
1.s1和s2长度相同,且所有字符完全相同,则s1==s2为真;
2.s1和s2所有字符不完全相同,则比较第一对不相同字符的ASCLL码,较小字符所在的串为较小的串;
3.如果s1长度n1小于s2的长度n2,且俩字符串前n1个字符串完全相同,则s1<s2;
一些有用的函数:
1)s.at(0)=‘H’;at访问下标,可以判断是否越界;
2)size()求长度;
3)bool empty()判断空;
4)string append(const char*s)相当于+=;
5)string assign(const char*s)相当于=
6)string& insert(unsigned int p0,const char*s)插入s到p0前;
7)string substr(unsigned int p0,const int n)const;取p0
前n个值作为返回对象;
8)unsign int find(const string &str)const;找str第一次出现的位置;
9)void swap(string& str);交换;
六.继承与派生
代码重用和扩展
1.类的继承与派生
派生类的定义:
class 派生类名:继承方式 基类1,继承方式 基类2...
{
派生类成员说明;
}
单继承、多继承;
可以新增成员来增加新的属性和功能;
生成过程:
吸收基类成员、改造基类成员(访问控制)、添加新成员(构造、析构)
2.访问控制
成员的存取控制:
继承方式 | 结果 |
---|---|
public | 私有变不可访问,其他不变 |
protected | 公有和保护都变成保护,私有变不可访问 |
private | 公有和保护变成私有,私有变不可访问 |
可通过继承的公有成员函数访问父类的私有成员;
3.类型兼容规则
在需要基类对象的任何地方,都可以使用公有派生类的对象来替代;
通过公有继承派生类得到了基类除构造、析构函数之外的所有成员;
兼容规则中所指的替代包括:
1)派生类对象可以隐含转换为基类对象:b1=d1,即用派生类对象中从基类继承来的成员,逐个赋值给基类对象成员(新增成员无法赋值);
2)派生类对象可以初始化基类的引用:B &rb=d1;
3)派生类的指针可以隐含转换为基类的指针:pb1=&d1;
视频中所举例子应用到了兼容性规则3),可以把b2给基类ptr指针,再用ptr调用继承下来但被隐藏的display函数
4.派生类的构造和析构函数
派生类的对象:
·使用前必须初始化,派生类的成员对象是由所有基类的成员对象与派生类的成员对象共同组成,构造派生类对象时,要对基类的对象成员和新增对象成员初始化;
·基类的构造函数不会继承,需要给派生类增加新的构造函数,派生对基类初始化,需要调用基类构造函数,派生类构造函数需要合适的初值作为参数
构造步骤:
♦调用基类的构造函数来初始化他们的数据成员;
♦构造函数初始化列表初始化派生类新增成员对象;
♦执行派生类构造函数函数体;
派生类:
语法形式:
派生类名::派生类名(参数表):基类名1(基类1初始化参数表)…成员对象名1(成员对象1初始化参数表)…
{派生类构造函数其他操作};
一个类有多个基类,对于所有需要给与参数初始化的基类,如果不是使用默认构造,必须显式的给出基类名和参数表;
什么时候要声明派生类的构造函数?
对基类初始化需要调用基类的带有形参表的构造函数,提供一个将参数传递给基类构造函数的途径,保证在基类进行初始化时,能够获得必要的数据;
步骤:
先基类初始化,按照初始化列表从左到右;
然后类的新增成员对象初始化;
最后函数体;
析构函数与此相反
5.派生类成员的标识与访问
作用域分辨符**::**
可见性原则:
♦如果在俩个或多个具有包含关系的作用域,外层声明了一个同名标识符,内层没有再次声明同名标识符,外层标识符在内层仍然可见;
♦如果在内层声明了同名标识符,则外层标识符在内层不可见,这时称内层标识符隐藏了外层同名标识符,这种现象称为隐藏规则;
简而言之:内隐藏外,内可见外;
对于多继承:
派生类成员将隐藏所有基类的同名成员;
使用“对象名.成员名”和“对象指针->成员名”
如果派生类没有声明同名成员,上述俩种方法无法唯一标识派生类继承的成员;此时只有使用::
如果出现多继承且一个父类是另一个父类的父类:
派生类中也会出现同名(有多个同名副本),必须用直接基类,使用作用域分辨符来限定;
更好的解决方案虚基类:
在派生类的对象中,同名数据成员在内存中有多个副本,同一个函数名有多个映射;此时,将共同基类设置为虚基类(virtual),这样从不同路径继承过来的同名数据成员,在函数中就只有一个副本,同一个函数名也只有一个映射;
注意:
如果虚基类声明有非默认形式及带形参的构造函数,并且没有声明默认形式的构造函数,那么在整个继承关系中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化列表中列出对虚基类的初始化;
不用担心会对虚基类多次初始化,C++编译器只会调用最远派生类的构造函数;
七.多态性
1.多态性概述
多种状态
绑定:
♦是计算机程序自身彼此关联的过程;
♦是把一个标识符名和一个存储地址联系在一起的过程;
♦是把一条消息和一个对象的方法相结合的过程;
**函数重载的问题:**是一种绑定;
多态分类:
静态多态:编译时的多态,在编译阶段就完成了(函数重载、运算符重载);
动态多态:运行时的多态,等到运行时才将标识符和相应函数代码结合;(虚函数);
2.双目运算符重载为成员函数
为什么要重载:将自定义类型进行运算;
基本语法规则:
1.只能存在C++已有的运算符;
2.以下运算符不能重载:. .* ->* :: sizeof ?:
类属关系运算符、成员指针运算符、作用域分辨符、sizeof、三目运算符;
3.重载后运算符优先级不变;
定义形式:
返回类型 operator 运算符(形参)
{
......
}
例:
Complex operator +(Complex &c)//情况一,类的成员函数
{...}
Complex operator +(Complex &c1,Complex &c2)//情况二,非成员函数(可以是友元函数)
{...}
重载的俩种形式如上俩种类型(访问私有数据可以通过友元和公有接口);
重载的方法:
具体以复数类举例(见七.3,九分钟左右);
3.单目运算符重载为成员函数
重载格式:
一般都是前置运算符,即运算符在前,操作数在后(如-a)负号:
此时直接重载为成员函数,不需要形参;
但是++、–既可能前置有可能后置,如何区分?
C++规定,在参数表中增加int这个标识符来区分;注意,int纯粹是为了区分,并不代表参数是int型,带有int参数时是后置形式;
例子(重载为成员函数):
Obj operator U();//前置单目
Obj operator U(int)//后置单目
这里举例复数类负号重载、时钟类前、后置++重载(七.4 四分钟左右);
这里后置++方法:
先设置同类old储存当前值(*this);
++(*this);
再返回old;
最好是直接调用前置++,可以避免出错;
4.运算符重载为非成员函数
使用情境:
左操作数不是本类对象(实数加复数);
左操作数不是程序员自定的类的对象(io);
具体语法:
要列出所有操作数;
后置++、–,操作数个数,形参列表中要增加一个int;
操作数至少有一个是自定义类型;
在运算符重载函数中可能需要操作某个类对象的私有成员时,可以将此函数声明为该类友元(或利用公有接口,但友元效率更高);
重载插入和提取运算符(友元):
举例:
ostream &operator << (ostream &out,complex &c)
{
out<<c.real<<c.img<<endl;
return out;
}
注意:
返回值必须是ostream对象的引用,因为要求重载后级连的输出,即:多个对象输出,可直接接连输出,返回值作为左值继续参与下一个运算,实现要求;
只能重载为成员函数:
=、()、[]、->
只能重载为非成员函数:
<<、>>
5.程序实例
1)重载逻辑非“!”
2)重载赋值“=” 深复制和浅赋值
如何实现深复制:
1.检查自赋值;
2.释放原有空间(当原数据不为空);
3.分配新空间;
4.复制内容;
5.返回*this;
几点关键理解:
1:避免自赋值(提高效率;防止是指针时出错),一般通过比较赋值者与被赋值者的地址是否相同;
5:被赋值的引用,即*this:
♦避免一次拷贝,提高了效率;
♦可以实现连续赋值,类似a=b=c;
如果不返回被赋值者的引用而是返回值类型,调用重载函数后将会进行一次拷贝,返回一个未命名的副本(匿名对象),而C++规定这种副本为右值,这就导致不能进行类似a=b=c连续运算;
6.虚函数
1.在函数前加上关键字virtual(只能出现在类定义的函数原型声明,而不能在成员函数实现时添加);
2.虚函数是运行多态,即动态多态的基础,它必须是动态绑定的函数,不能是静态的成员函数;虚函数应该属于对象而不是整个类,他需要在运行时用指针定位到它指向的对象是谁,然后决定调用哪个函数;
3.虚函数具有继承性;
4.虚函数最好不要设为内联函数(内联编译时就处理了,是静态);
5.调用方式:通过基类指针或引用,执行时会根据指针指向/引用的对象的类,决定调用哪个函数;(这一节有程序的演示)
动态多态的条件和实现过程:
♦将派生类对象赋值给基类指针(或引用);
♦通过基类指针(或引用)调用虚函数;
♦根据传递的派生类的指针(或引用)的不同实现访问的多态性;
把派生类对象地址传给基类指针,此时实现基类指针指向派生类,如何实现基类虚函数调用?
♦使用::
♦ptr->A::display()//仍指向基类函数
对象切片:派生类对象复制构造基类对象的行为;
此时.display()不需要动态绑定,对象类型名明确;
虚析构函数:
派生类通过基类指针调用对象的析构函数来delete动态开辟的内存,就需要让基类的析构函数称为虚函数,否早会产生不确定的后果。
1,实现指针引用时动态绑定,实现运行时多态
2,保证使用基类基类类型的指针能调用适当的析构函数针对不同对象进行清理工作;
7.抽象类
纯虚函数:函数由于信息不够具体而无法实现;
声明形式:
virtual 函数类型 函数名 (参数表)=0
virtual void fun()=0;
纯虚函数时一类在基类中声明的虚函数,无法具体实现(在基类中定义的信息不够具体,导致这个函数没法规定具体算法);
声明为纯虚函数,基类可用不再给出函数体;
对于普通函数:纯虚函数的函数体由派生类给出(若基类给出实现,则必须由派生类覆盖);
对于析构函数:必须在基类给出实现;
**抽象类:**带有纯虚函数的类,不能产生实例;
作用:
♦规定对外接口的统一形式:
可以使我们将基类对象和各级不同派生类的对象都按照统一方式进行处理;
**如果没有实现纯虚函数,派生类继续作为抽象类,没法定义对象;实现了纯虚函数,有了函数体,就可以定义对象;这说明抽象类是作为基类使用的,是不能够定义对象的;**不能传对象,可以传引用;
动态的多态的是把不同的派生类的对象传递给了基类的指针;
八.模板
♦使用一种通用的方法来设计函数或者类;
♦它不具体指定操作对象的数据类型,通过使用可变的数据类型来使使用可变的数据类型来时代码适用于不同的数据类型;
♦减少重复代码编写,实现代码重用;
我把它看作重载的进阶;
1.函数模板
定义:
♦由模板参数说明和函数定义组成;
♦模板类型参数说明由template开头,后面接尖括号括起来的“模板参数表”;
♦模板参数表可以包含逗号隔开的多个模板参数;
工作原理:
只是抽象说明,实例化后称为模板函数才能执行;
例子:
#include <iostream>
using namespace std;
template<class T>
T Max(T a,T b){return a>b?a:b;}
int main()
{
int x,y;
cin>>X>>y;
cout<<Max(x,y);
return 0;
}
-
类型参数class可以替换为typename替代;
-
template语句与函数模板定义语句见不允许有其他语句;
-
函数模板实例化可以隐式和显式实例化;
-
模板参数表中可以出现非模板类型参数,直接接受一个由“类型说明符”所规定的常量作为其参数;
OutputArray<int,10>(a); template<class T,int count> void OutputArray(const T *array) { for(int i=0;i<count;<i++)cout<<array[i]<<""; cout<<endl; }
5.函数模板可以重载;形参表或类型参数表不同;
6.函数模板和函数的优先顺序:
在有多个函数和函数模板名字相同的情况下,编译器处理一条函数调用语句的顺序:
先找参数完全匹配的普通函数(非模板函数);
再找参数完全匹配的模板参数;
再找实参经过自动类型转换后能够匹配的普通函数;
上面都找不到就报错;
2.类模板
主要用于数据存储类或容器类或容器类,类的数据类型和算法不受所包含元素影响;
举例;对动态数组类抽象;
定义:
类似于函数模板。
♦模板类型参数说明由template开头,后面接尖括号括起来的“模板参数表”,后面紧接类的定义;
♦模板参数表和函数模板一样;
♦类的定义中,成员数据和成员函数声明普通函数,只是涉及到数据类型的时候需要用到模板的类型参数;
再类模板外定义成员函数,格式:
template <模板参数表>
返回值类型 类模板名<模板参数标识符列表>::成员函数(参数表)
{......}
实例化:
类似于模板函数的显示实例化:
类模板名<实际的模板类型参数表>对象名;(八.2五分钟,注意外部函数定义方法)
实例化:Pair<string,int> s1(“tom”,10),s2(“mary”,11);
注意事项:
同一个类模板生成的若干模板类是相互不兼容的;
函数模板可以作为类模板的成员;(实例见本节6:51);
类模板的“<类型参数表>"中可以出现非模板类型参数,实例化时,非模板类型参数必须是指定类型的常量值;
九.流类库与输入输出
1.流的概念:数据的流动
标准输入流对象:cin;
标准输出流对象:cout、cerr和clog(标准日志输出)
输入输出重定向
文件输入输出流
文件分类:
文本文件是以字符为单位储存的文件,可以用文本编辑器编辑;
二进制文件是以数据为单位,可以是字符型、整型、实型;
二进制文件可以跨平台,但是不同平台换行符的自动处理有区别;
文件操作步骤:
通过文件流类的默认构造函数定义文件流对象;
通过文件流对象的成员函数open打开相应文件;
通过文件流类的成员函数对文件进行读写操作;
通过成员函数close关闭文件;
十.异常处理
使程序有容错。
1.异常处理的思想
什么是异常:
编译错误:由编译器指出错误,按照指引修改即可;
运行错误:环境条件发生意外或者用户使用操作不当,使得程序运行出现的错误就称为异常;
常见的异常:
cpu异常、内存异常、设备异常、用户数据异常、代码bug
异常处理的方法:终止程序、用函数返回值作为异常标志;
最好的方法:利用c++异常处理机制
try-throw-catch三部曲
2.异常处理的实现
throw语句抛出异常,try语句异常检测,catch语句捕获异常;
1)抛出异常throw语句
throw抛出异常->本函数中处理->主调函数处理
**语法:**throw后加上表达式(C++跟据异常类型区分不同异常)
throw抛出异常传递给catch;
说明:
抛出异常,程序流程将直接转移到匹配的catch语句;
抛出的异常实际上是一个”异常对象“。异常对象由throw创建,传递给对应的catch语句,异常处理完成后该异常对象将自动撤销;
如果抛出数组,自动转为数组首元素指针;
抛出函数,转化为该函数指针;
抛出指向派生类对象的基类指针,返回基类部分;
抛出指针必须确保指针指向的对象在异常处理结束之前在内存中一直存在,否则就是错误的;
2)检查捕获异常try-catch语句
♦try块和catch块要一起出现;
♦try块的复合语句是有可能抛出异常的代码;
♦try块后面可以紧跟多个catch块;
♦每个catch块可以捕获try块抛出的一种类型的异常;
异常处理的类型匹配:
catch块声明和抛出的类型相同;
catch块声明是抛出的引用;
声明是抛出的基类或引用;
声明的异常类型和抛出的异常类型都是指针类型,且前者可以隐含转换为后者;
执行过程:
try块无异常就执行最后一个catch块后语句;
有异常就创造异常对象,匹配异常并处理,若异常不在try块内或找不到匹配的catch块,则结束当前函数,按照异常传递方向,回到当前函数的调用点,把调用点作为异常抛出点,重复匹配过程直至捕获,如果最后程序实在匹配不到则C++调用terminate终止程序;
注意:
如果一个函数抛出了异常接口不允许的异常,系统自动调用unexpected函数终止程序;
用户可以自定义unexpected函数;
异常接口声明的格式:
1)void fun() throw(A,B);//能抛出A和B类型的异常
2)void fun() throw();//不抛出任何类型的异常
3)void fun()//能抛出任何异常
异常处理后,异常处理将把从开始进入try块到异常抛出这段期间构造的所有对象全部析构;
3.标准异常库异常处理
详见C++异常之七 标准库里的异常类 - 索智源 - 博客园 (cnblogs.com)
标准异常类:
exception类派生出的7大子类:
其作用:
使用这些标准异常类,必须加上相应头文件;
一般情况下都是:
#include<stdexcept>
引用异常类&e后使用 cerr<<e.what()可以输出错误信息;
(全篇完)
2023/6/18
明天考C++!!!狠狠的过!