Cpp语言程序设计
Cpp语言程序设计
JJuprising第一章 绪论
机器语言与汇编语言
由计算机硬件系统可以识别的二进制指令组成的语言称为机器语言。
汇编语言将机器指令映射为一些可以被人读懂的助记符,如ADD、SUB等。
高级语言
- 高级语言屏蔽了机器的细节,提高了语言的抽象层次,程序中可以采用具有一定含义的数据命名和容易理解的执行语句。这使得在书写程序时可以联系到程序所描述的具体事物。
面向对象的语言
- 出发点:更直接地描述客观世界中存在的事物(对象)以及它们之间的关系。
- 特点:
- 是高级语言。
- 将客观事物看作具有属性和行为的对象
- 通过抽象找出同一类对象的共同属性和行为,形成类。
- 通过类的继承与多态实现代码重用
- 优点:使程序能够比较直接地反映问题域的本来面目,软件开发人员能够利用人类认识事物所采用的一般思维方法来进行软件开发。
面向对象的方法
- 将数据及对数据的操作方法封装在一起,作为一个相互依存、不可分离的整体——对象。
- 对同类型对象抽象出其共性,形成类。
- 类通过一个简单的外部接口,与外界发生关系。
- 对象与对象之间通过消息进行通信。
计算机中的信息
- 数据信息——计算机程序加工的对象
- 控制信息——指挥计算机操作
- 信息的存储单位
- 位(bit,b):度量数据的最小单位,表示一位二进制信息。
- 字节(byte,B):由八位二进制数字组成(1 byte = 8 bit)。
- 千字节 1 KB = 1024 B
- 兆字节 1 MB = 1024 K
- 吉字节 1 GB = 1024 M
- 进制转换
第二章 C++简单程序设计
I/O流
- 在C++中,将数据从一个对象到另一个对象的流动抽象为“流”。流在使用前要被建立,使用后要被删除。
- 从流中获取数据的操作称为提取操作,向流中添加数据的操作称为插入操作。
- 数据的输入与输出是通过I/O流来实现的,cin和cout是预定义的流类对象。cin用来处理标准输入,即键盘输入。cout用来处理标准输出,即屏幕输出。
第三章 函数
static_cast是一个强制类型转换操作符
1 | double a = 1.999; |
函数的声明的实现
1 | void func(int);//函数的声明,可以没有形参名,没有大括号 |
函数的参数传递
- 在函数被调用时才分配形参的存储单元
- 实参可以是常量、变量或表达式
- 实参类型必须与形参相符或可隐式转换为形参类型
- 值传递传递参数值,即单向传递
- 引用传递可以实现双向传递
- 常引用作参数可以保障实参数据的安全
内联函数
- 声明时用关键字
inline
- 规模小、功能简单使用频繁的函数。编译时在调用处嵌入函数体,节省了参数传递、控制转移等开销
- 注意
- 内联函数体内不能有循环语句和switch语句
- 内联函数的定义必须出现在内联函数第一次被调用之前
- 对内联函数不能进行异常接口声明
函数高级
1.默认参数
默认参数,如果我们自己传入数据,就用自己的数据,如果没有,那么用默认值。
注意事项:
- 有默认参数的要放在最后面。
int fun(int a,int b,int d=1){};
- 如果函数声明有默认参数,函数实现就不能有默认参数。因为声明时已经定义过了,两个有就冲突了。声明和实现只能有一个有默认参数
- 有默认参数的要放在最后面。
2.占位参数
返回值参数 函数名(数据类型){}
- void func(int a, int);后面的int就是占位参数,只有传两个才能正常执行。
- 占位参数也能有默认默认参数,这时候就可以不传这个占位参数了。
3.函数重载
作用:函数名可以相同,提高复用性
满足条件
- 同一作用域下
- 函数名称相同
- 函数参数类型不同 或者 个数不同 或a 顺序不同
- 对返回值没有规定
1 | int sumOfSquare(int a,int b){ |
注意:
- 函数的返回值和形参名不可以作为函数重载的条件
- 函数重载碰到默认参数会出现二义性,尽量避免这种情况。
引用
引用相当于给变量取别名。语法:
数据类型 &别名=原名;
引用声明时就必须初始化,
int &c;
是错误的在声明一个饮用后,不能再使之作为另一变量的引用(指向不可修改)
引用作为重载的条件。
- 通过引用参数产生的效果同按地址传递是一样的,引用的语法更加清楚简单,简化指针修改实参。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//指针作参数
void swap1(int *a,int *b){
int temp=*a;
*a=*b;
*b=temp;
}
//引用作参数
void swap2(int &a,int &b){
int temp=a;
a=b;
b=temp;
}
int main(){
int a=10;
int b=20;
swap1(&a,&b);//指针参数函数的调用,地址传递
swap2(a,b);//引用传递
}void func(int & a);void func(const int & a);
当func(a)
时走第一个,当func(10)
走第二个。const
是只读状态,相当于创建块区域,然后引用的指向它。注意int &a=10;
是不合法的,10是一个常量,故不会走第一个。
1 | // 声明简单的变量 |
结果:
1 | Value of i : 5 |
- 引用做函数的返回值
- 不要返回局部变量的引用
- 函数的调用可以作为左值存在
1 | int & test02(){ |
第四章 类和对象
类和对象
C++面向对象的三大特性为:封装、继承、多态。万事万物都皆为对象,对象上有其属性和行为
具有相同性质的对象,我们可以抽象为类。
封装
- class代表设计一个类,类后面紧跟着的是类的名称
1 | class Circle |
- 实例化:通过类创建一个具体对象。通过”.”来访问,可以给属性赋值
1 | Circle p1;//和结构体类似 |
- 类中的属性和行为统称为 成员。属性: 成员属性/成员变量。行为: 成员函数/成员方法。
访问权限:
公共权限 public 成员 类内(class大括号内)可以访问 类外也可以访问
保护权限 protected 成员 类内可以访问 类外不可以访问 儿子可以访问父亲的保护内容
私有权限 private 成员 类内可以访问 类外不可以访问 儿子不可以访问父亲的私有内容
struct和class区别
- 唯一区别:默认的访问权限不同。struct 默认是公共public,class默认是私有private
建议成员属性设置为私有。
- 原因:1.可以自己控制读写权限 2.对于写可以检测数据的有效性
可以通过public里的行为对private里的属性进行修改,实现只读、可读可写、只写。
读
1 | string getlover(){ |
- 写:
1 | void setlover(string lover){ |
- 检测有效性:
1 | //经过检测才能修改到,不至于直接修改private的属性造成麻烦 |
- 在类中可以让另一个类作为 本类中的成员
- 作用域::成员函数 类的声明.h,需要
#pragma once
和#include<iostream>
和using namepace
,如果这个类中还用到另一个类,需要引用另一类的头文件然后加作用域 。 类的实现.cpp需要#include "_.h"
以及在函数名前加作用域,不需要外部的class和public和private的属性。 - 对象特性
- 构造函数(对象初始化)和析构函数(对象清理)
构造函数和析构函数
构造函数语法:类名(){}
- 构造函数,没有返回值也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数语法:~类名(){}
- 析构函数,没有返回值也不写void
- 函数名称与类名相同,在名称前加符号~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无需手动调用,而且只会调用一次
以上两个都是必须有的实现,如果不提供,编译器会提供,不过是空实现。
- 构造函数的分类及调用
两种分类方法
- 按照参数分类 无参构造(默认构造,编译器提供的)和有参构造
- 按照类型分类 普通构造和复制构造函数
- 复制构造函数写法
1 | Person(const Person &p){ |
三种调用方法
1.括号法
Person p1; //默认构造函数调用
Person p2(10); // 有参构造函数
Person p3(p2); //拷贝构造函数
- 注意事项1:默认构造函数调用时不要加(),编译器会误认为函数的声明
2.显示法
Person p1;//不需要加括号写成Person p1()
Person p2 = Person(10); //有参构造
Person p3 = Person(p2); //拷贝构造
- person(10); //是匿名对象 特点:当前行执行结束后,系统会立即回收匿名对象
- 注意事项2:不要利用拷贝构造函数 初始化匿名对象,如person(p3);编译器会识别出person p3;
3.隐式转换法,直接写
1 | Person p4 = 10; //相当于写了 Person p4 = Person(10); |
拷贝构造函数的调用时机
1.使用一个已经创建完毕的对象来初始化一个新对象
1 | Person p1; |
2.值传递的方式给函数传值
1 | void fun1(Person p){} |
3.以值方式返回局部对象
1 | Person doword(){ |
构造函数的调用规则
默认情况下,编译器至少给一个类添加三个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
- 如果写了有参构造函数,编译器就不提供默认构造,但仍提供拷贝构造
- 如果写了拷贝构造函数,编译器就不提供其他函数
归纳
1 | //构造函数 可分为无参构造(默认构造)和有参构造;或者分成普通构造和拷贝构造 |
构造与析构顺序
1 |
|
- B类中有对象A作为成员,A为对象成员
- 当创建B对象是,会先调用对象成员A的构造函数,再调用B的构造函数
- 而析构顺序相反,先析构B再析构A,所谓先构造后析构,后构造先析构
深拷贝与浅拷贝
浅拷贝:编译器的默认的简单的复制拷贝操作 深拷贝:在堆区重新申请空间(new),进行拷贝操作
- 浅拷贝的问题(类中有指针,释放的时候同一块地方被两个类释放两次,非法)要用深拷贝解决
1 | //拷贝函数 |
如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题
初始化列表
作用:初始化属性 语法:构造函数():属性1(值1),…{ }
类对象作为类成员
暂时用列表传参
C++运算符的重载
运算符重载的概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
- 加法运算符的重载
第五章 数据的共享与保护
作用域
局部作用域
对象生存期
静态生存期
- 这种生存期与程序的运行期相同
- 在文件作用域中声明的对象具有这种生存期
- 在函数内部声明静态生存期对象,要冠以关键字
static
动态生存期
- 在局部作用域中声明的具有动态生存期的对象,习惯上也被称为局部生存期对象
- 局部生存期对象诞生于声明点,结束于声明所在的块执行完毕之时
常对象
常对象必须进行初始化,且不能被更新
语法:const 类型说明符 对象名;
1 | class A{ |
用const修饰的类成员
常成员函数
声明格式:类型说明符 函数名(参数表)const;
注意:
- 函数定义的时候也要加上const
- 常成员函数调用期间不能更新(修改)对象的数据成员,也不能常成员函数中调用没有用const修饰的成员函数
常数据成员
使用const说明的数据成员为常数据成员,初始化后不能修改。构造函数对该数据成员进行初始化就只能通过初始化列表
1 | class A{ |
常引用
1 | int &a=b;//相当于int*const a=b,指向不可改变,指针常量 |
使得引用的对象只读,不能通过a来改变b的值
静态变量与静态函数(存在于全局,并不属于特定的哪个对象)
静态数据成员
静态数据成员:使得一个类的所有对象具有相同的属性,对于任何对象实例,它的属性值相同,不属于任何一个对象。(具体看例子)
注意:
- 由于静态数据成员不属于任何一个对象,因此可以通过类名对他访问,一般用法:
类名::标识符
- 静态数据成员需要在类定义之外再加以定义。原因:以此来专门为它们分配空间。非静态数据成员无须,因为他们的空间是与他们所属对象的空间同时分配的
1 | class Point{ |
创建不同Point
类对象a
和b
可以分别调用showCount
函数输出同一个count
在不同时刻的数值,实现了a,b之间直接的数据共享。
静态函数成员
静态成员函数可以访问静态成员变量
静态成员函数不可以访问非静态成员变量 ,无法区分到底是哪个对象的
访问可以通过成员也可以通过类名Person::func();
这样就不需要创建一个对象然后通过对象的成员函数来访问成员了。
在静态成员函数中没有this指针,因为它属于整个类而不是具体的哪个对象,this指向的是具体的对象
1 | //上面的例子做些修改 |
成员变量 和 成员函数 是分开存储的
- 空对象占用的内存空间为1字节,为了区分不同的空对象占用的空间
- 非静态成员变量 属于类的对象上,而静态成员变量、非静态成员函数、静态成员函数都不属于类的对象上。
1 | class Person{ |
this指针
- this指针概念
成员函数和成员变量分开存储,每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码,那么这一块代码是如何区分是哪个对象调用自己呢?
this指针指向被调用的成员函数所属的对象不需要定义,直接用
- 解决名称冲突
1 | Person(int age){ |
返回对象本身用
return *this
空指针调用成员函数.如果要用的话,成员函数里面不能有属性,否则报错,因为传入空指针,this是NULL,或者是成员函数里前面加个
1
2if(this==NULL)
return;
const修饰成员函数
this指针本质是一个指针常量,不能修改指向 :
1 | Person * const this;//指向不能改。 |
常函数
1 | void showp() const |
- 成员函数后加
const
称为常函数 - 常函数内不可修改成员属性
- 在成员函数后面加
const
修饰的是this
指针,让指针指向的值也不能改,相当于这个函数加了const
就是承诺不修改this
指向的属性。 mutable int m_B;
加上关键字mutable
就是特殊变量,在常函数中可以修改
常对象
1 | const Person p;//在对象前加const,变为常对象,一般的成员变量不能改 |
- 同理加了mutable就可以改
- 常对象只能调用常函数,防止你用常对象调用普通函数来修改里面的属性
常数据成员
常数据成员只能通过初始化列表来获得初值
1 | class A{ |
常引用:即只读状态
1 | void dist(const Point &a); |
const型数据小结
形式 | 含义 |
---|---|
Point const t1 |
t1 是常对象,其值在任何情况下都不能改变 |
void Point::func() const |
func() 是Point 类中的常成员函数,可以引用,但不能修改成员 |
Point * const p |
p 是指向Point 类对象的常指针,p 的值不能改变,即指向不能变 |
const Point *p |
p 是指向Point 类常对象的指针,其指向的类对象的值不能通过指针来改变 |
Point &t1=t; |
t1 是Point 类对象t 的引用,二者指向同一段内存空间 |
友元
让一个函数或者类访问另一个类中私有成员和保护成员
注意:
- 友元的关系是单向的而不是双向的
- 友元的关系不能传递
- 全局函数作右元
1 | class Room{ |
- 类做友元
1 | class Building; |
另一个类的成员函数做友元
实操经验:如果是一个A类的成员变量想做B类的友元,那么B类里要声明友元,格式 :
friend 函数类型 A::函数名();
同时,B类的声明要放在A类之后,否则编译器找不到A::函数名()这个东西friend声明友元函数,友元函数却依旧无法访问该类的私有属性”的解决
一次C++作业题, 搞了很久弄明白了, 虽然成功了, 但VS2015依旧有红线提示错误, 不过不影响编译、运行, 这似乎是VS自身的一个BUG。
解决:
友元类方法小结:- 包含声明”friend”的类,必须在((包含其声明的友元函数)的那个类)之前事先声明下————因为在Employer类中用到”Employee&”,不然无法访问该引用的私有成员。
- 被声明为友元的函数,必须在类内声明,然后在将其声明为友元函数的类的后面定义。
- 还有一个我个人犯的低级错误——在声明友元函数时,忘记加该函数的作用域了。。。
作业代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using namespace std;
class Employee;//先声明,因为在Employer中会用到,否则不给友元函数访问Employer的私有
class Employer
{
public:
Employer(string a) {
Name = a;
};
void editEmployee(Employee & person, double salary, string post);
private:
string Name;
};
class Employee {
public:Employee(int a, string b, double c, string d) {
ID = a;
Name = b;
Salary = c;
Post = d;
}
friend void Employer::editEmployee(Employee & person, double salary, string post);
void printInf() {
cout << "ID:" << ID << "\t" << "Name:" << Name << "\t" << "Salary:" << Salary << "\t" << "Post:" << Post << endl;
}
protected:
private:
int ID;
string Name;
double Salary;
string Post;
};
void Employer::editEmployee(Employee & person, double salary, string post) {
person.Salary = salary;
person.Post = post;
};
第六章 数组 指针与字符串
数组是具有一定顺序关系的若干相同类型变量的集合体,组成数组的变量称为该数组的元素。
数组
二维数组初始化
将所有初值写在一个{}内,按顺序初始化
- 例如:
static int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
- 例如:
分行列出二维数组元素的初值
- 例如:
static int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
- 例如:
可以只对部分元素初始化
- 例如:
static int a[3][4]={{1},{0,6},{0,0,11}};
- 例如:
列出全部初始值时,第1维下标个数可以省略
- 例如:
static int a[][4]={1,2,3,4,5,6,7,8,9,10,11,12};或:static int a[][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
- 例如:
注:
如果不作任何初始化,局部作用域的非静态数组中会存在垃圾数据,static数组中的数据默认初始化为0
如果只对部分元素初始化,剩下的未显式初始化的元素,将自动被初始化为零
对象数组初始化
1 | Point a[2]={Point(1,2),Point(3,4)}; |
- 数组中每一个元素对象被创建时,系统都会调用类构造函数初始化该对象
- 元素所属的类不声明构造函数,则采用默认构造函数。
- 当数组中每一个对象被删除时,系统都要调用一次析构函数。
指针
内存空间的访问方式
- 通过变量名访问
- 通过地址访问
指针的概念
- 指针:内存地址,用于间接访问内存单元
- 指针变量:用于存放地址的变量
指针名=地址
- C++11使用
nullptr
关键字,是表达更准确,类型安全的空指针
指向常量的指针和指针类型的常量
指向常量的指针(常指针)
声明时const
在最前面。不能通过指针来改变指向对象的值,但是指针本身指向可以改变
1 | int a; |
指针类型的常量(指针常量)
const
在*
后,指向不可改变。
1 | int a; |
指针类型的算术运算
指针p加上或减去n
- 其意义是指针当前指向位置的前方或后方第n个数据的起始位置。
指针的++、–运算
- 意义是指向下一个或前一个完整数据的起始。
运算的结果值取决于指针指向的数据类型,总是指向一个完整数据的起始位置。
当指针指向连续存储的同类型数据时,指针与整数的加减和自增自减算才有意义。
指针与数组
指针名加了整数再用*解引用得到所指对象的值。
定义指向数组元素的指针
定义与赋值
1
2
3
4int a[10], *pa;
pa=&a[0]; //或 pa=a;数组名a地址也是数组第一个元素a[0]的地址
*pa就是a[0],*(pa+1)就是a[1],... ,*(pa+i)就是a[i].
a[i], *(pa+i), *(a+i), pa[i]都是等效的。注意:不能写 a++,数组名不能自加自减,因为a是数组首地址、是常量。
字符串
字符数组
用于存放字符串的数组其元素个数应该不小于字符串的长度(即字符个数)加1,因为要在末尾放置一个’\0’。
1 | char str[8]={'p','r','o','g','r','a','m'}; |
动态内存分配
目的:保证程序在运行过程中按照实际需要申请适量的内存,使用结束后还可以释放。
在C++程序中建立和删除堆对象使用两个运算符,new
和delete
new
new
的功能是动态分配内存,语法:new 数据类型 (初始化参数列表)
创建一维数组:new 类型名 [数组长度];
- 如果内存申请成功,
new
运算便返回一个指向新分配内存首地址的类型的指针,可以通过这个指针对堆对象进行访问 new T
和new T()
效果相同,都会调用这个默认构造函数
1 | int *point; |
delete
释放指针所指向的内存空间,语法:delete 指针名;
删除数组要在指针名前面加”[]”:delete []指针名;
- 如果是对象,会调用对象的析构函数
- 对于用
new
建立的对象只能执行一次delete
删除操作
1 | delete point; |
内存四区
代码区 全局区 栈区 堆区
c++中在程序运行前分为全局区和代码区
代码区
特点是共享和只读。共享目的是对于频繁被执行的程序只需要保存一份代码即可
全局区
全局变量、静态变量、字符串常量、const修饰的全局变量存放在全局区
局部修饰的都不在全局区里
常量分为字符串常量和const修饰的变量,const修饰的变量有全局也有局部
栈区
有编译器自动分配释放,存放函数的参数值、局部变量等
注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
堆区
由程序员分配释放,程序结束时由操作系统回收
在c++中用关键字new
将数据开辟到堆区,返回值是地址,如 new int(10),
将10放到堆区里,可指针接。在程序运行时,10一直存在。
释放利用delete
指向该区域的指针。
1 | int *p=new int(10); |
常见一个数组用中括号:int *arr=new int[10];
上面的()表示只有一个元素
释放 delete[] arr;
第七章 类的继承
基类和派生类
graph TD; 交通工具-->火车 交通工具-->汽车 交通工具-->飞机 交通工具-->轮船 汽车-->卡车 汽车-->旅行车 汽车-->小汽车 小汽车-->工具车 小汽车-->轿车 小汽车-->面包车
- 从已有类产生新类的过程就叫类的派生
- 派生类(子类)包含了基类(父类)特征,同时可以加入自己所特有的新特征
- 一个派生类同时有多个基类的情况称为多继承(有多个爹),只有一个直接基类叫做单继承
- 在类族中,直接参与派生出某类的基类称为直接基类(爸爸辈),跨层的基类称为间接基类(爷爷辈及以上)。如图中汽车是卡车、旅行车、小汽车的直接基类,而交通工具是旅行车的间接基类
派生类构造函数和析构函数
构造函数
例题
1 | //例7-4.cpp |
输出结果
构造函数的调用顺序:先调用基类的构造函数,然后调用内嵌对象的构造函数
①基类构造函数的调用顺序是按照派生类定义时继承的顺序,如例题
1 | class Derived :public Base2, public Base1, public Base3{ |
因此是先Base2,再Base1,最后Base3.
②而内嵌对象的构造函数调用顺序应该是按照成员在类中声明的顺序
1 | private://派生类的私有成员对象 |
应该是先Base1,再Base2,最后Base3.
因此结果顺序是2-1-3-1-2-3
析构函数
语法:~类名(){}
- 析构函数不接受任何参数
- 如果不显式说明,系统会自动生成
上个例题的析构结果是:
析构函数顺序和构造函数是严格相反的,因此会先对派生类新增的类类型的成员对象进行清理,最后对所有从基类继承来的成员进行清理
三种继承方式
- 公有继承,基类的公有和保护成员的访问属性在派生类中不变,私有的在类外无法直接访问
- 私有继承,基类中的公有成员和保护成员都以私有成员身份出现在派生类中,而基类的私有成员在派生类中不可直接访问。经过多轮私有继承之后,所有的基类成员都成为派生类的私有成员或不可直接访问成员,基类的成员无法发挥作用,相当于终止了派生,使用较少
- 保护继承,基类中的公有成员和保护成员都以保护成员身份出现在派生类中,而基类的私有成员不可直接访问。派生类的其他成员就可以直接访问从基类继承来的公有和保护成员,但在类外部通过派生类无法直接访问它们。与私有继承差别就是基类的保护成员可能被它的派生类访问(不至于无法发挥作用),同时保证其绝对不可能被其他外部使用者访问。(某些需要被保护起来的成员对子孙有用时可以被用到)
类型兼容规则
- 派生类的对象可以隐含转换为基类对象,即可以用派生类对象赋值给基类对象。
- 派生类的对象可以初始化基类的引用
- 派生类的指针可以隐含转换为基类的指针
以上称为向上转型。
不要重新定义同名的非虚函数,因为此时派生类调用重新定义的非虚函数时都只能访问到从基类继承来的那个最原始的成员。
不能被继承
C++中,不能被派生类继承的是: 构造函数
私有继承调用基类
1 | //7-8.cpp |
派生类成员的标识与访问
作用域分辨符
当某派生类的多个基类拥有同名的成员时,调用同名成员必须通过基类名和作用域分辨符“:”来标识成员
1 | int main(){ |
虚基类 virtual
同名的数据成员在内存中拥有多个副本,需要使用作用域分辨符来唯一标识并访问它们。将共同基类设置为虚基类,这时从不同的路径继承过来的同名数据成员在内存中就只有一个,同一个函数名也只有一个映射,避免冗余。
语法形式:class 派生类名:virtual 继承方式 基类名
上述语句声明基类为派生类的虚基类,一起维护同一个内存数据
在类Derived中d.Base1::var0
和d.Base2::var0
是一个对象,造成冗余
虚继承:
1 | class Base1: virtual public Base0{//类Base1是类Base0的公有派生类,Base0是Base1的虚基类 |
因此访问呢只需d.var0
最远派生类
就是最年轻的那个子孙,后面没有再派生了
最远基类
最老的那个基类
虚基类及其派生类构造函数
如果最远虚基类中没有默认构造但是有有参构造,那么它的每一个子孙都必须在构造函数的成员初始化列表中为最远虚基类的构造函数列出参数。如果未列出表示调用虚基类默认构造函数,又因为没定义,所以会报错
如果最远派生类构造函数调用虚基类的构造函数,那么其他类对虚基类构造函数的调用将被忽略
例:
注意:如果不可预估此基类会派生多少子类,那没必要用虚继承。同时多继承非必要不使用,来避免冗余。
第八章 多态性
多态:指同样的消息被不同类型的对象接收时导致不同的行为,即调用了不同的函数
多态性是指具有不同功能的函数可以用同一个函数名,这样就可以用一个函数名调用不同内容的函数
多态分为两类:
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
虚函数
什么是虚函数?
- 在基类用
virtual
声明成员函数为虚函数
虚函数的作用:
- 虚函数的作用是允许在派生类中重新定义与基类同名的函数(且能同时存在),并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
- 而对于派生类的同名函数来说,它覆盖了继承来的基类的同名函数,发挥自己的功能,解决了在第七章类型兼容规则中的问题
使用方法:
- 基类声明成员函数前加关键字
virtual
,实现时不用加virtual
- 在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型全部与基类的虚函数相同,并根据派生类的需要重新定义函数体
- C++规定,当一个成员函数被声明为虚函数后,其派生类中的同名函数都 自动成为虚函数,可以不加
virtual
上面例题是想通过基类的指针指向派生类的对象,并访问某个与基类同名的成员,那么首先在虚类中将这个同名函数说明为虚函数。
多态满足的条件:
- 有继承关系
- 子类重写父类中的虚函数
初识虚函数
- 用
virtual
关键字说明的函数 - 动态绑定的函数
- 不能是内联,要在类外实现,因为对内联函数的处理是静态的
虚表
在Derived
中新定义了f(),会覆盖Base::f
,其实就是重新开一个新函数;**没有定义g()来覆盖基类,故在虚表中查找g()会指向基类的g()**。
virtual关键字
- 如果基类函数是虚函数,派生类有同名的函数,默认为虚函数可以不用加
virtual
,自动覆盖基类同名函数。 - 想要覆盖基类同名函数,习惯添加
virtual
,增加可读性
哪些成员函数可以是虚函数
- 一般非静态成员函数可以是
- 构造函数不具有多态功能,不能是
- 析构函数可以是
纯虚函数
纯虚函数是在声明虚函数是被“初始化”为0的函数,没有定义具体的操作内容,甚至没有函数体。要求各派生类根据实际需要定义自己的版本。声明格式
1 | virtual 函数类型 函数名(参数表) = 0; |
- 纯虚函数没有函数体,不需要实现,即没有
{}
- 最后面的
=0
并不表示函数返回值为0,它只是告诉编译器这是纯虚函数 - 用途是当基类不知道或者不需要这个函数有具体的意义无法实现但是派生类可以进行实现
抽象类
带有纯虚函数的类是抽象类。有函数但是不实现。用于初步设计,信息抽象暂时不实现。
抽象类只能是基类
1 | class 类名{ |
- 凡是包含纯虚函数的类都是抽象类
- 一个基类如果包含一个或一个以上纯虚函数,就是抽象基类
- 抽象类不能实例化,即不能定义一个抽象类的对象
运算符重载
c++中有以下五个运算符不能重载
成员访问运算符 | 成员指针访问运算符 | 域运算符 | 长度运算符 | 条件运算符 |
---|---|---|---|---|
. | .* | :: | sizeof | ?: |
重载运算符规则:
- 重载不能改变运算符运算对象(即操作数)的个数
- 重载不能改变运算符的优先级别
- 重载不能改变运算符的结合性
- 重载运算符的函数不能有默认的参数,否则就改变了运算符参数的个数,与1矛盾
- 重载运算符必须和用户定义的自定义类型的对象一起使用,其参数至少应有一个是类对象。也就是说,参数不能全部是C++的标准类型,以防止用户修改用于标准类型数据的运算符的性质
- 运算符重载函数可以是类的成员函数,也可以是类的友元函数,还可以是既非类的成员函数也不是友元函数的普通函数。
单目运算符
当使用重载运算符c1+c2
就相当于是c1.operator+(c2)
,重载+左操作数就是本类。
双目
目标:经过重载后,相当于oprd1.operator 运算符(oprd2)
,oprd1
要是随意的一个类而不像单目那样是本类。
重载++,–
- 前置单目运算符,重载函数没有形参
- 后置运算符,重载函数需要一个int形参(为了区分,加一个形参) 编译器编译成
oprd.operator ++ (0)
前置运算符重载函数类型是引用,返回的是*this
后置运算符重载函数类型是类,返回值是一个局部类变量。如果此时函数类型错写成引用,试想一下引用指向的是一个即将消亡的局部变量……
第九章 模板与全体数据
模板
函数模板
函数体是一样的,定义形式
1 | template<模板参数类型> |
template
,声明创建模板typename
,表明其后面的符号是一种数据类型,可以用class
代替T
,通用的数据类型,名称可以替换,通常为大写字母- 编译器通过实参类型推导函数模板的类型参数,以模板生成一个函数,称为函数的实例化
注意:
- 一个函数模板并非自动可以处理所有类型的数据,只有能够进行函数模板中运算的类型,可以作为类型实参
- 函数模板只适用于函数体相同、函数的参数个数相同而类型不同的情况,如果参数的个数不同,则不能用函数模板
- 自定义的类需要为该类重载模板中的运算符,才能作为类型实参
1 | //求绝对值 |
类模板
把T
做替换成传入的参数
注意使用模板要加上尖括号和实参 类名<>看作整体类名来用
例:
1 | //9_2 |
结构体成员快速初始化 大括号
群体
线性群体
直接访问的线性群体——数组
- 动态数组如
vector
元素个数可以在程序运行时改变
顺序访问的线性群体——链表
上图例子了两个版本的[]运算符重载,const的为了能修改常对象。返回的常引用对象(函数名前有const
)只能读不能写。不能写参数和返回值
类内数组深层复制一般需要重载“=”运算符
- 避免自身复制
if(&rhs!=this)
- 比较数组大小是否相同,new(不相同则删除原有,重新分配
- 遍历数组一个一个复制
return *this
链表
概念:链表是一种动态数据结构,可以用来表示顺序访问的线性群体。链表是由系列结点组成的,结点可以在运行时动态生成。每个结点包括数据域和指向链表中下一个结点的指针(即下一个结点的地址)。如果链表每一个结点中只有一个指向后继结点的指针,则该链表称为单链表。
如果每个结点中有两个用于连接其他结点的指针,一个指向前趋结点(称前趋指针),另一个指向后继结点(称后继指针),则构成双向链表。链表中的第一个结点称为头结点,最后一个结点称为尾结点,尾结点的后继指针为空。
插入结点
data1的结点存放着data2节点的地址,要先把data2结点的地址给新节点然后再把新结点的地址给data1结点,顺序不能乱。
删除结点
- 要看是不是最后的结点
- 实现起来还要加一个前驱节点的地址,只有用前一个previous结点才能删除现在遍历到的current结点。
栈
概念:生活中的例子,假设餐厅里有一摞盘子,如果我们要从中拿取盘子,只能从上面一个开始拿,当我们要放上一个盘子是也只能放在最上面。栈的结构正是如此,每个盘子相当于栈中的一个数据,数据只能从栈的一端存入(“压入栈”),并且只能从栈的同一端取出(“弹出栈”),这一端叫栈顶,而栈的另一端叫作栈底。栈中的数据的添加和删除操作具有”后进先出“(LIFO)的特性,也就是说,栈中的所有数据,越早被压入的(接近栈底的),就越晚被弹出。
表达式处理
读取输入流,左边放数值,右边放运算符,运算符入栈的时候如果优先级低(如a-b加号优先级低于/),那么不能入栈,这时弹出栈中运算符(如/)同时弹出对应操作数的数值进行运算,结果重新放回数值栈中,重复操作。
栈的基本操作
- 初始化
- 入栈
- 出栈
- 清空栈
- 把栈顶top置为-1
- 访问栈顶元素
- 检查栈的状态(满、空)
队列
概念:柜台前、收款机前排队。队列是只能向一端添加元素,从另一端删除元素的线性群体,在队尾添加元素,在队头删除元素。在队头位置的标记成为队头指针,对队尾位置的标记称为队尾指针。向队尾添加元素称为”入队”,删除队头元素称为”出队”。”先进先出”(FIFO),最早入队的最先出队。
排序与查找
插入排序(从后往前检索)
比较的时候如果不满足停止条件,需要给key元素腾出空间,找到之后可以直接插入
1 | //用直接插入排序法对数组A中的元素进行升序排列 |
选择排序
1 | //辅助函数:交换x和y的值 |
交换排序
每一轮沉底一个最大元素,n个元素最多排序n-1次,即沉底n-1个元素。
一轮循环结束后lastExchangeIndex
其实就是沉好底的元素最上面那个还没排的元素的索引
1 | //辅助函数:交换x和y的值 |
二分查找
当找不到数,注意结束的条件(左边界要是中间数加一,右边界是中间数减一)
1 | /用折半查找方法,在元素呈升序排列的数组list中查找值为key的元素 |
第十章 泛型程序设计与C++语言标准模板库
面向对象三个特性:封装、继承、多态
STL 标准模板库
六大组件:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器
容器可以嵌套容器,里面的叫元素,分为序列式容器和关联式容器:
序列式容器:强调值的顺序,有固定顺序
关联式容器:二叉树结构,各元素之间没有严格的物理上的顺序关系
迭代器,用来遍历元素的指针。实际上迭代器是一个类,这个类封装了一个指针
算法,通过有限的步骤,解决问题。
质变算法:运算过程中改变区间内元素内容,如拷贝替换查找。
非质变算法:不更改内容,如查找、计数、遍历
仿函数,行为类似函数,可作为算法的某种策略。
适配器,一种用来修饰容器或者仿函数活迭代器接口的东西
空间适配器,负责空间的配置与管理
迭代器是算法和容器的桥梁,使算法能够作用到容器。理解为提供给算法函数的指针参数。
容器算法迭代器初识
vector容器存放内置数据类型
创建:
1 | vector<int>v; |
向容器尾部插入数据:
1 | v.push_back(10); |
通过迭代器访问容器中的数据:
1 | vector<int>::iterator itBegin=v.begin();//起始迭代器,指向容器中第一个元素 |
第一种遍历方式:
1 | while(itBegin!=itEnd){ |
第二种遍历方式(常用):
1 | for(vector<int>::iterator it=v.begin();it!=v.end();it++){ |
第三种用算法库:
1 | //需要加 |
vector存放自定义数据
存放指针类型
1 | vector<Person*>v;//Person是一个类,存放Person类型的指针 |
遍历
1 | for(vector<Person*)::iterator it=v.begin();it!=v.end();it++){ |
存放容器类型,容器嵌套
1 | vector<int>v; |
遍历,需要两层循环
1 | for(vector< vector<int>::iterator it=V.begin();it!=V.end();it++){ |
string容器
基本概念
string
是一个类,封装了一个char*
来维护,是一个char*
容器
1 | string s1; |
vector容器
单端数组,可动态扩展
动态拓展:并不是在原空间之后来连续接新空间,而是找更大的内存空间,然后将原数据拷贝到新空间,释放原空间
set容器
概念
set
不允许插重复的,multiset
可以。实现自动升序排序
1 |
|
set大小和交换
函数原型
1 | size();//大小,set没有resize()重新设置大小的操作,因为当扩大时其余未设定的会补0导致重复 |
set插入和删除
函数原型
1 | s1.insert();//插入 |
set查找和统计
map容器
map中所有元素都是pair
pair中第一个元素为key(键值),第二个元素为value(实值)
所有元素都会根据元素的键值自动排序
本质属于关联式容器,底层结构二叉树实现
优点:可以通过key快速找到value值
map容器构造和赋值
1 | map<int,int>m;//创建需要两个参数,对组pair元素 |
总结:map中所有元素都是成对出现,输入数据时需要使用对组
map容器大小和交换
1 | size();//大小 |
函数对象
函数对象(仿函数)本质是个类,而不是一个函数
谓词
仿函数 返回值类型是bool数据类型,称为谓词
一元谓词
如果operator()
接受一个参数,那么叫做一元谓词,两个参数叫二元谓词
1 | find_if(v.begin(),v.end(),查询条件);//返回值是v相同类型的迭代器,没找到返回的是v.end() |
第十一章 流类库与输入输出
输出流概述
最重要的三个输出流ostream
,ofstream
,ostringstream
预先定义的ostream类对象用来完成向标准设备的输出:
cout
是标准输出流cerr
是标准错误输出流,没有缓冲,发送给它的内容立即被输出clog
类似cerr
ofstream
类支持磁盘文件输出
使用width控制输出宽度
1 |
|
使用setw操纵符指定宽度
1 | //11_2.cpp |
设置对齐方式
1 | //11_3.cpp |
向二进制文件输出
1 | //11_5.cpp |
向字符串输出
1 | //11_6.cpp |
输入流
重要的输入流类:
- istream类最适合用于顺序文本模式输入。cin是其实例。
- ifstream类支持磁盘文件输入。
- istringstream
构造输入流对象
- 如果在构造函数中指定一个文件名,在构造该对象时该文件便自动打开。
1 | ifstream myFile("filename"); |
- 在调用默认构造函数之后使用open函数来打开文件
1 | ifstream myFile;//建立一个文件流对象 |
- 打开文件时可以指定模式
1 | ifstream myFile("filename",ios_base::in|ios_base::binary); |
相关函数
open函数把该流与一个特定磁盘文件相关联。
get函数的功能与提取运算符(>>)很相像,主要的不同点是get函数在读入数据时包括空白字符。(第6章介绍过)
getline的功能是从输入流中读取多个字符,并且允许指定输入终止字符,读取完成后,从读取的内容中删除终止字符。(第6章介绍过)
read成员函数从一个文件读字节到一个指定的内存区域,由长度参数确定要读的字节数。当遇到文件结束或者在文本模式文件中遇到文件结束标记字符时结束读取。
seekg函数用来设置文件输入流中读取数据位置的指针。
tellg函数返回当前文件读指针的位置。
close函数关闭与一个文件输入流关联的磁盘文件。
输入流举例应用
为输入流指定一个终止字符
利用getline函数
1 | //11_8.cpp |
istringstream将字符串转换为数值
1 | //11_12.cpp, 头部分省略 |
两个重要的输入/输出流
一个iostream对象可以是数据的源或目的。
两个重要的I/O流类都是从iostream派生的,它们是fstream和stringstream。这些类继承了前面描述的istream和ostream类的功能。
fstream类
- fstream类支持磁盘文件输入和输出。
- 如果需要在同一个程序中从一个特定磁盘文件读并写到该磁盘文件,可以构造一个fstream对象。
- 一个fstream对象是有两个逻辑子流的单个流,两个子流一个用于输入,另一个用于输出
stringstream类
- stringstream类支持面向字符串的输入和输出
- 可以用于对同一个字符串的内容交替读写,同样是由两个逻辑子流构成
第十二章 异常处理
异常处理的语法
若有异常则通过
throw
创建一个异常对象并抛掷将可能抛出异常的程序段嵌在
try
块之中。通过正常的顺序执行到达try
语句,然后执行try
块内的保护段如果在保护段执行期间没有引起异常,那么跟在
try
块后的catch
子句就不执行。程序从try
块后的最后一个catch
子句后面的语句继续执行catch
子句按其在try
块后出现的顺序被检查。匹配的catch
子句将捕获并处理异常(或继续抛掷异常)。如果匹配的处理器未找到,则库函数terminate将被自动调用,其默认是调用abort终止程序。