C++语言程序设计(第4版)
第一章 绪论
1.1 面向对象的基本概念
1.1.1 对象
面向对象方法中的对象,是系统中用来描述客观事务的一个主体,它是用来构成系统的一个基本单位。对象是由一组属性和一组行为构成的。
1.1.2 类(数据类型)
分类所依据的原则是抽象。面向对象方法中的“类”,是具有相同属性和服务的一组对象的集合。
1.1.3 封装
封装是面向对象方法中的一个重要原则,就是把对象的属性和服务组合成一个独立的系统单位,并尽可能隐蔽对象的内部细节。
1.1.4 继承
特殊类的对象拥有其一般类的全部属性与服务,称做特殊类对一般类的继承。
1.1.5 多态性
多态性是指一般类中定义的属性或行为,被特殊类继承之后,可以具有不同的数据类型或表现出不同的行为。
第二章 C++简单程序设计
2.1 C++的特点
C++语言的主要特点是:1.尽量兼容C;2.支持面向对象的方法。
x.cpp:源程序,x.obj:目标程序,x.exe:执行程序。
2.2 基本数据类型
类型名 | 长度(字节) | 取值范围 |
---|
bool | 1 | false,true | char | 1 | -128~127 | signed char | 1 | -128~127 | unsigned char | 1 | 0~255 | short(signed short) | 2 | -32768~32767 | unsigned short | 2 | 0~65535 | int(signed int) | 4 | -2147483648~2147483647 | unsigned int | 4 | 0~4294967295 | long(signed long) | 4 | -2147483648~2147483647 | unsigned long | 4 | 0~4294967295 | float | 4 | 3.4x10(-38)~3.4x1038 | double | 8 | 1.7x10(-308)~1.7x10308 | long double | 8 | 1.7x10(-308)~1.7x10308 |
一般而言,如果对一个整数所占字节数和取值范围没有特殊要求,使用int型为宜,因为它通常具有最高的处理效率。
2.3 变量的存储类型
auto:暂时性存储。采用堆栈方式分配内存空间,其存储空间可以被若干变量多次覆盖使用。
register:存放在通用寄存器中。
extern:在所有函数和程序段中都可以引用。
static:在内存中以固定地址存放,整个程序运行期间都有效。
2.4 运算方式和表达式
2.4.1 算数运算式与算数表达式
2.4.2 赋值运算符与赋值表达式
2.4.3 逗号运算和逗号表达式
2.4.4 逻辑运算与逻辑表达式
优先级分两级:(<,<=,>,>=) > (==,!=)。
“&&”和“||”具有“短路”特性,如果第一个操作数求值后为false,则不再对第二个操作数求值。
2.4.5 条件运算符与条件表达式
表达式1?表达式2:表达式3。
2.4.6 sizeof运算符
sizeof(类型名),用于计算某种类型的对象在内存中所占的字节数。
2.4.7 位运算
(1)按位与&:a=a&0xfe将a最低位变为0,c=a&0xff取出a的低字节;
(2)按位或|:a=a|0xff将a最低位变为1;
(3)按位异或^;
(4)按位取反~;
(5)移位<<或>>:2<<1=4,左边表达式的值不会被改变。
第三章 函数
C++继承了C语言的全部语法,也包括函数的定义与使用方法。
3.1 函数的定义与使用
主函数是程序执行的开始点,由主函数调用子函数,子函数还可以调用其他子函数。
3.1.1 函数的定义
形参的作用是实现主调函数与被调函数之间的联系,通常将函数所处理的数据、影响函数功能的因素或者函数处理的结果作为形参。
只有函数被调用时才由主调函数将实际参数(实参)赋予形参。
一个函数可以有返回值,也可以没有返回值,没有返回值的时候类型标识符是void,可以不写return语句,也可以写一个不带表达式的return表示结束当前函数的调用。
3.1.2 函数的调用
在调用函数前,要声明或定义该函数。
3.1.3 函数的参数传递
值传递:单向传递;
引用传递:可以实现双向传递,声明引用的时候必须同时对它初始化,指向一个已存在的对象,并且不能更改;
常引用作参数可以保障实参数据的安全。
3.2 内联函数
声明时使用关键字 inline。
编译时在调用处用函数体进行替换,节省了参数传递、控制转移等开销。 注意:
内联函数应是比较简单的函数。
内联函数体内不能有循环语句和switch语句。
内联函数的声明必须出现在内联函数第一次被调用之前。
对内联函数不能进行异常接口声明。
3.3 带默认参数值的函数
3.3.1 有默认参数值的形参必须放在最后:
int add(int x, int y = 5, int z = 6);//正确
int add(int x = 1, int y = 5, int z);//错误
int add(int x = 1, int y, int z = 6);//错误
3.3.2 在相同的作用域内,不允许在用一个函数的多个生命中对同一个参数的默认值重复定义,即使前后定义的值相同也不行:
int add(int x = 5,int y = 6);
//原型声明在前
int main() {
add();
}
int add(int x,int y) {
//此处不能再指定默认值
return x + y;
}
int add(int x/* = 5*/,int y/* = 6*/) {
//只有定义,没有原型声明
return x + y;
}
int main() {
add();
}
(好习惯:在参数表中以注释来说明参数的默认值)
3.4 函数重载
定义:两个以上的函数,具有相同的函数名,但是形参的个数或者类型不同,编译器根据实参和形参的类型及个数的最佳匹配,自动确定调用哪一个函数。
重载函数的形参必须不同:个数不同或类型不同。
当使用具有默认形参值得函数重载形式时,需要注意防止二义性:
void fun(int length,int width = 2,int height = 33);
void fun(int length);
在调用的时候如果使用以下形式就会报错:
fun(1);
3.5 补充
在C++中声明函数时后面括号内为空,表示的是要求的参数是未知的,如果没有函数,后面括号内应该填上void。
第四章 类与对象
4.1 面向对象程序设计的基本特点
4.1.1 抽象
抽象是对具体对象(问题)进行概括,抽出这一类对象的公共性质并加以描述的过程。
抽象的实现:通过类的声明。
4.1.2 封装
封装就是将抽象得到的数据和行为(或功能)相结合,形成一个有机整体,也就是将数据与操作数据的函数代码进行有机的结合,形成“类”,其中的数据和函数都是类的成员。
实现封装:类声明中的{}。
4.1.3 继承
实现:声明派生类——见第7章
4.1.4 多态
多态:同一名称,不同的功能实现方式。
目的:达到行为标识统一,减少程序中标识符的个数。
实现:重载函数和虚函数——见第8章
4.2 类和对象
在面向对象程序设计中,程序模块是由类构成的。类是对逻辑上相关的函数与数据的封装,它是对问题的抽象描述。
4.2.1 类的定义
类包括数据成员和函数成员
数据成员的访问控制属性有三种:public、protected、private
函数成员的访问控制属性有四种:public、protected、friendly、private
4.2.2 内联成员函数
隐式声明:将函数体放在类的声明中。 显式声明:使用inline关键字。
4.3构造函数和析构函数
4.3.1 构造函数
构造函数的作用是在对象被创建时使用特定的值构造对象,将对象初始化为一个特定的初始状态。 在对象创建时被自动调用。 如果程序中未声明,则系统自动产生出一个默认的构造函数,其参数列表为空。 构造函数可以是内联函数、重载函数、带默认参数值的函数。
4.3.2 复制构造函数
https://blog.csdn.net/shuaiyoutiao/article/details/121497260?spm=1001.2014.3001.5502
4.3.3 析构函数
析构函数是在对象的生存期即将结束的时刻被自动调用的。
析构函数不接受任何参数,但可以是虚函数。
4.4类的组合
4.4.1 组合
一个类中内嵌其他类的对象作为成员,它们之间的关系是一种包含与被包含的关系。
声明形式: 类名::类名(对象成员所需的形参,本类成员形参) :对象1(参数),对象2(参数),… { //函数体其他语句 }
组合类构造函数定义的一般形式是:
类名::类名(形参表):内嵌对象1(形参表),内嵌对象2(形参表)…
{类的初始化}
#include <iostream>
#include <cmath>
using namespace std;
class Point { //Point类定义
public:
Point(int xx = 0, int yy = 0) {
x = xx;
y = yy;
}
Point(Point &p);
int getX() { return x; }//类外实现
int getY() { return y; }//类外实现
private:
int x, y;
};
Point::Point(Point &p) { //复制构造函数的实现
x = p.x;
y = p.y;
cout << "Calling the copy constructor of Point" << endl;
}
//类的组合
class Line { //Line类的定义
public: //外部接口
Line(Point xp1, Point xp2);
Line(Line &l);
double getLen() { return len; }
private: //私有数据成员
Point p1, p2; //Point类的对象p1,p2
double len;
};
//组合类的构造函数
Line::Line(Point xp1, Point xp2) : p1(xp1), p2(xp2) {
cout << "Calling constructor of Line" << endl;
* double x = static_cast<double>(p1.getX() - p2.getX());//强制类型转化
double y = static_cast<double>(p1.getY() - p2.getY());//强制类型转化
len = sqrt(x * x + y * y);
}
Line::Line (Line &l): p1(l.p1), p2(l.p2) {//组合类的复制构造函数
cout << "Calling the copy constructor of Line" << endl;
len = l.len;
}
//主函数
int main() {
Point myp1(1, 1), myp2(4, 5); //建立Point类的对象
Line line(myp1, myp2); //建立Line类的对象
Line line2(line); //利用复制构造函数建立一个新对象
cout << "The length of the line is: ";
cout << line.getLen() << endl;
cout << "The length of the line2 is: ";
cout << line2.getLen() << endl;
return 0;
}
生成两个Point类的对象->构造Line类的对象line->复制构造line2->输出两点距离。在整个过程中Point类的复制构造函数被调用了6次,而且都是在Line类构造函数体之前进行的。
运行结果如下: Calling the copy constructor of Point Calling the copy constructor of Point Calling the copy constructor of Point Calling the copy constructor of Point Calling constructor of Line Calling the copy constructor of Point Calling the copy constructor of Point Calling the copy constructor of Line The length of the line is: 5 The length of the line2 is: 5
4.4.2 前向引用声明
例子:请定义两个类A和B,其中A类中含有B类的对象b1, B类中含有A类的对象a1,编写两个类的构造函数和主函数,其中,在A类的构造函数中输出“b1构造成功”, 在B类的构造函数中输出“a1构造成功”;
#include <iostream>
#include <cmath>
using namespace std;
class B;
class A
{
public:
A(){cout << "A默认构造成功" << endl;}
A(int l){cout << "b1构造成功" << endl;}
private:
B *b;//只能声明指针或引用,如果改为B b则编译错误
};
class B
{
public:
B(){cout << "B默认构造成功" << endl;}
B(int l){cout << "a1构造成功" << endl;}
private:
A a;
};
int main()
{
A al(1);
B b1(1);
A a2;
}
4.5 结构体和联合体
4.5.1 结构体
结构体和类的唯一区别在于,结构体和类具有不同的默认访问控制属性,类是private,而结构体是public。
4.5.2 联合体
联合体的全部数据成员共享同一组内存单元,所以联合体变量成员中至多只有一个有意义。
例:
#include <string>
#include <iostream>
using namespace std;
class ExamInfo {
private:
string name; //课程名称
enum { GRADE, PASS, PERCENTAGE } mode;//采用何种计分方式
union {
char grade; //等级制的成绩
bool pass; //只记是否通过课程的成绩
int percent; //百分制的成绩
};
public:
//三种构造函数,分别用等级、是否通过和百分初始化
ExamInfo(string name, char grade)
: name(name), mode(GRADE), grade(grade) { }
ExamInfo(string name, bool pass)
: name(name), mode(PASS), pass(pass) { }
ExamInfo(string name, int percent)
: name(name), mode(PERCENTAGE), percent(percent) { }
void show();
}
void ExamInfo::show() {
cout << name << ": ";
switch (mode) {
case GRADE: cout << grade; break;
case PASS: cout << (pass ? "PASS" : "FAIL"); break;
case PERCENTAGE: cout << percent; break;
}
cout << endl;
}
int main() {
ExamInfo course1("English", 'B');
ExamInfo course2("Calculus", true);
ExamInfo course3("C++ Programming", 85);
course1.show();
course2.show();
course3.show();
return 0;
}
运行结果:
English: B
Calculus: PASS
C++ Programming: 85
第五章 数据的共享与保护
5.1 标识符的作用域与可见性
5.1.1 作用域
作用域是一个标识符在程序正文中有效的区域。
1.函数原型作用域:
C++中最小的作用域,始于“(”,终于“)”,例double area(double radius);
2.局部作用域:
3.类作用域:
类X的成员m具有类作用域,访问方法有三种:
第一种是如果在X的成员函数中没有声明同名的局部作用域标识符,那么在该函数内可以直接访问成员m;
第二种是通过表达式x.m或X::m;
第三种是通过ptr->m,ptr为指向X类的一个对象的指针。
4.命名空间作用域:
例子
(具有命名空间作用域的变量也成为全局变量)
#include <iostream>
using namespace std;
int i; //全局变量,文件作用域
namespace Ns{
int j; //在Ns命名空间中的全局变量
}
int main() {
i = 5; //为全局变量i赋值
Ns::j=6; //为全局变量j赋值
{ //子块1
using namespace Ns;//使得在当前块空可以直接引用Ns命名空间的标识符
int i; //局部变量,局部作用域
i = 7;
cout << "i = " << i << endl;//输出7
cout << "j = " << j << endl;//输出6
}
cout << “i = ” << i << endl;//输出5
return 0;
}
运行结果:
i = 7
j = 6
i = 5
5.1.2 可见性
程序运行到某一点,能够引用到的标识符,就是该处可见的标识符。
标识符应声明在先,引用在后。 如果某个标识符在外层中声明,且在内层中没有同一标识符的声明,则该标识符在内层可见。 对于两个嵌套的作用域,如果在内层作用域内声明了与外层作用域中同名的标识符,则外层作用域的标识符在内层不可见。
5.2 对象的生存期
对象从诞生到结束的这段时间就是它的生存期。
5.2.1 静态生存期
如果对象的生存期与程序的运行期相同,则称它具有静态生存期。
方法:在命名空间作用域声明或在函数内部的局部作用域中声明时加上static关键字。
局部作用域中的静态变量特点:不会随着每次函数调用而产生一个副本,也不会随着函数返回而失效,当下一次函数调用时,该变量保持上一回的值。
5.2.2 动态生存期
在局部作用域中具有动态生存期的对象,习惯上称为局部生存期对象。局部生存期对象诞生于声明点,结束于声明所在的块执行完毕时。
#include<iostream>
using namespace std;
int i = 1; // i 为全局变量,具有静态生存期。
void other() {
static int a = 2;
static int b;
// a,b为静态局部变量,具有全局寿命,局部可见。
//只第一次进入函数时被初始化。
int c = 10; // C为局部变量,具有动态生存期,
//每次进入函数时都初始化。
a += 2; i += 32; c += 5;
cout<<"---OTHER---\n";
cout<<" i: "<<i<<" a: "<<a<<" b: "<<b<<" c: "<<c<<endl;
b = a;
}
int main() {
static int a;//静态局部变量,有全局寿命,局部可见,未指定初值,被赋予0值初始化。
int b = -10; // b, c为局部变量,具有动态生存期。
int c = 0;
cout << "---MAIN---\n";
cout<<" i: "<<i<<" a: "<<a<<" b: "<<b<<" c: "<<c<<endl;
c += 8;
other();
cout<<"---MAIN---\n";
cout<<" i: "<<i<<" a: "<<a<<" b: "<<b<<" c: "<<c<<endl;
i += 10; other();
return 0;
}
运行结果:
---MAIN---
i: 1 a: 0 b: -10 c: 0
---OTHER---
i: 33 a: 4 b: 0 c: 15
---MAIN---
i: 33 a: 0 b: -10 c: 8
---OTHER---
i: 75 a: 6 b: 4 c: 15
5.3 类的静态成员
静态成员可以解决同一个类的不同对象之间数据和函数共享问题。
5.3.1 静态数据成员
用关键字static声明
为该类的所有对象共享,静态数据成员具有静态生存期。
必须在类外定义和初始化,用(::)来指明所属的类。
//5_4.cpp
#include <iostream>
using namespace std;
?
class Point { //Point类定义
public: //外部接口
Point(int x = 0, int y = 0) : x(x), y(y) { //构造函数
//在构造函数中对count累加,所有对象共同维护同一个count
count++;
}
Point(Point &p) { //复制构造函数
x = p.x;
y = p.y;
count++;
}
~Point() { count--; }
int getX() { return x; }
int getY() { return y; }
void showCount() { //输出静态数据成员
cout << " Object count = " << count << endl;
}
private: //私有数据成员
int x, y;
static int count; //静态数据成员声明,用于记录点的个数
};
//重要点
int Point::count = 0;//静态数据成员定义和初始化,使用类名限定
int main() { //主函数
Point a(4, 5); //定义对象a,其构造函数回使count增1
cout << "Point A: " << a.getX() << ", " << a.getY();
a.showCount(); //输出对象个数
//可换成Point::showCount();
?
Point b(a); //定义对象b,其构造函数回使count增1
cout << "Point B: " << b.getX() << ", " << b.getY();
b.showCount(); //输出对象个数
return 0;
}
运行结果:
Point A: 4, 5 Object count=1
Point B: 4, 5 Object count=2
5.3.2 静态函数成员
静态成员函数可以直接访问该类的静态数据和函数成员,而访问非静态成员,必须通过对象名。
#include <iostream>
using namespace std;
class Point { //Point类定义
public: //外部接口
Point(int x = 0, int y = 0) : x(x), y(y) { //构造函数
//在构造函数中对count累加,所有对象共同维护同一个count
count++;
}
Point(Point &p) { //复制构造函数
x = p.x;
y = p.y;
count++;
}
~Point() { count--; }
int getX() { return x; }
int getY() { return y; }
static void showCount(Point &p) { //静态函数成员
cout << " Object count = " << count << endl;
cout << "x = " << p.x << endl;
}
private: //私有数据成员
int x, y;
static int count; //静态数据成员声明,用于记录点的个数};
?int Point::count = 0;//静态数据成员定义和初始化,使用类名限定
?int main() { //主函数
Point a(4, 5); //定义对象a,其构造函数回使count增1
cout << "Point A: " << a.getX() << ", " << a.getY();
Point::showCount(a); //输出对象个数和a的x
? Point b(a); //定义对象b,其构造函数回使count增1
cout << "Point B: " << b.getX() << ", " << b.getY();
Point::showCount(a); //输出对象个数和a的x
? return 0;}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OTCb3vYr-1638624426767)(C:\Users\你爸爸\AppData\Roaming\Typora\typora-user-images\image-20211129163814727.png)]
如果把static void showCount(Point &p)改为static void showCount(Point p),则在生成临时对象的时候count+1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rb6wLIuq-1638624426768)(C:\Users\你爸爸\AppData\Roaming\Typora\typora-user-images\image-20211129163519928.png)]
5.4 类的友元
友元破坏了数据隐蔽和封装,但是提高了程序的效率和可读性。
5.4.1 友元函数
友元函数是在类声明中由关键字friend修饰说明的非成员函数,在它的函数体中能够通过对象名访问 private 和 protected成员。
访问对象中的成员必须通过对象名。
friend float dist(Point &a, Point &b);
在main中使用:
dist(myp1,myp2);
5.4.2 友元类
若一个类为另一个类的友元,则此类的所有成员都能访问对方类的私有成员。
class A {
friend class B;//设置友元类
public:
void display() {
cout << x << endl;
}
private:
int x;
}
class B {
public:
void set(int i);
void display();
private:
A a;};
void B::set(int i) {
a.x=i;//在B的成员函数中可以访问a类对象的私有成员
}
void B::display() {
a.display();
}
注意:友元关系不可传递;友元关系是单向的;友元关系不可被继承。
5.5 共享数据的保护
对于既需要共享、又需要防止改变的数据应该声明为常类型(用const进行修饰)。对于不改变对象状态的成员函数应该声明为常函数。
5.5.1 常对象
常对象必须初始化,而且不能被更新。
class A
{
public:
A(int i,int j) {x=i; y=j;}
...
private:
int x,y;
};
A const a(3,4); //a是常对象,不能被更新(存放在全局变量栈中,不是局部函数栈)
5.5.2 用const修饰的类成员
1.常成员函数
常成员函数说明格式:类型说明符 函数名(参数表)const;
常成员函数不更新对象的数据成员。
这里,const是函数类型的一个组成部分,因此在实现部分也要带const关键字。
const关键字可以被用于参与对重载函数的区分:如果仅以const关键字为区分对成员函数重载,那么通过非const的对象
通过常对象只能调用它的常成员函数。
#include <iostream>
#include <cmath>
using namespace std;
class Point {
public:
Point(int xx, int yy) { x = xx, y = yy; }
int getX()const;//必须声明常成员函数,否则常对象无法访问
int getY()const;//同上
double Dis(Point p);
double Dis(const Point p) const;
private:
int x;
int y;
};
inline int Point::getX()const {
return x;
}
inline int Point::getY()const {
return y;
}
double Point::Dis(Point p) {
int xx = x - p.getX();
int yy = y - p.getY();
return sqrt(xx*xx + yy*yy);
}
double Point::Dis(const Point p)const {
int xx = x - p.getX();
int yy = y - p.getY();
return sqrt(xx*xx + yy*yy);
}
int main() {
const Point myp1(1, 1);
const Point myp2(4, 5);
cout << "常对象(1,1) and (4,5) Dis: " << myp1.Dis(myp2) << endl;
cout << "下面开始构造一般对象" << endl;
int x1, y1, x2, y2;
cout << "x1 and y1:";
cin >> x1 >> y1;
cout << "x2 and y2:";
cin >> x2 >> y2;
Point p1(x1, y1),p2(x2, y2);
cout << "一般对象 Dis: " << p1.Dis(p2) << endl;
system("pause");
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rWzK9Tzj-1638624426769)(C:\Users\你爸爸\AppData\Roaming\Typora\typora-user-images\image-20211129165638762.png)]
2.常数据成员
使用const说明的数据成员。
3.常饮用
声明形式:const 类型说明符 &引用名;
friend float dist(const Point &p1, const Point &p2);‘
//在main中使用:
const Point myp1(1, 1), myp2(4, 5);
dist(myp1, myp2);
5.6 多文件结构和编译预处理命令
5.6.1 C++的一般组织结构
在规模较大的项目中,往往需要多个源程序文件,每个类都有单独的定义和实现文件,采用将类的定义写入头文件中,要使用该类的.cpp文件包含该头文件的方法,可以对不同的文件进行单独编写、编译,最后再连接,同时充分利用类的封装特性,在调试过程中只修改一个类的定义和实现,而其余部分不改动。
//文件1,类的定义,Point.h
class Point { //类的定义
public: //外部接口
Point(int x = 0, int y = 0) : x(x), y(y) { }
Point(const Point &p);
~Point() { count--; }
int getX() const { return x; }
int getY() const { return y; }
static void showCount(); //静态函数成员
private: //私有数据成员
int x, y;
static int count; //静态数据成员
};
//文件2,类的实现,Point.cpp
#include "Point.h"
#include <iostream>
using namespace std;
?
int Point::count = 0; //使用类名初始化静态数据成员
?
Point::Point(const Point &p) : x(p.x), y(p.y) { //复制构造函数体
count++;
}
?
void Point::showCount() {
cout << " Object count = " << count << endl;
}
//文件3,主函数,5_10.cpp
#include "Point.h"
#include <iostream>
using namespace std;
?
int main() {
Point a(4, 5); //定义对象a,其构造函数回使count增1
cout << "Point A: " << a.getX() << ", " << a.getY();
Point::showCount(); //输出对象个数
?
Point b(a); //定义对象b,其构造函数回使count增1
cout << "Point B: " << b.getX() << ", " << b.getY();
Point::showCount(); //输出对象个数
?
return 0;
}
5.6.2 外部变量和外部函数
//源文件1
int i = 3; //定义变量i
void next();//函数原型声明
int main(){
i++;
next();
return 0;
}
void next(){
i++;
other();
}
//源文件2
extern int i;//声明一个在其他文件中定义的变量i
void other(){
i++;
}
5.6.3标准C++库
如果不加入语句“using namespace std;”就需要在每个标识符前加上“std::”
第六章 数组、指针与字符串
6.1 数组
6.1.1数组的声明
类型说明符 数组名[ 常量表达式 ] [ 常量表达式 ]…… ;
6.1.2 数组的存储和初始化
数组元素在内存中是顺序,连续存储的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X1NqReUz-1638624426770)(C:\Users\你爸爸\AppData\Roaming\Typora\typora-user-images\image-20211130151535674.png)]
二维数组的初始化 将所有数据写在一个{}内,按顺序赋值 例如: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}};
6.1.3 数组作为函数参数
使用数组名传递数据时,传递的是地址。
#include <iostream>
using namespace std;
void rowSum(int a[][4], int nRow) {
for (int i = 0; i < nRow; i++) {
for(int j = 1; j < 4; j++)
a[i][0] += a[i][j];
}
}
int main() { //主函数
int table[3][4] = {{1, 2, 3, 4},
{2, 3, 4, 5}, {3, 4, 5, 6}};
//声明并初始化数组
//输出数组元素
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++)
cout << table[i][j] << " ";
cout << endl;
}
rowSum(table, 3); //调用子函数,计算各行和
//输出计算结果
for (int i = 0; i < 3; i++)
cout << "Sum of row " << i << " is " << table[i][0] << endl;
return 0;
}
6.1.4 对象数组
声明: 类名 数组名[元素个数]; 访问方法: 通过下标访问 数组名[下标].成员名
数组中每一个元素对象被创建时,系统都会调用类构造函数初始化该对象。
//Point.h
#ifndef _POINT_H
#define _POINT_H
class Point { //类的定义
public: //外部接口
Point();
Point(int x, int y);
~Point();
void move(int newX,int newY);
int getX() const { return x; }
int getY() const { return y; }
static void showCount(); //静态函数成员
private: //私有数据成员
int x, y;
};
#endif //_POINT_H
//Point.cpp
#include <iostream>
#include "Point.h"
using namespace std;
Point::Point() {
x = y = 0;
cout << "Default Constructor called." << endl;
}
Point::Point(int x, int y) : x(x), y(y) {
cout << "Constructor called." << endl;
}
Point::~Point() {
cout << "Destructor called." << endl;
}
void Point::move(int newX,int newY) {
cout << "Moving the point to (" << newX << ", " << newY << ")" << endl;
x = newX;
y = newY;
}
//6-3.cpp
#include "Point.h"
#include <iostream>
using namespace std;
int main() {
cout << "Entering main..." << endl;
Point a[2];
for(int i = 0; i < 2; i++)
a[i].move(i + 10, i + 20);
cout << "Exiting main..." << endl;
return 0;
}
运行结果:
Entering main...
Default Constructor called.//调用无参构造函数
Default Constructor called.//调用无参构造函数
Moving the point to (10, 20)//调用成员函数move,i=0
Moving the point to (11, 21)//调用成员函数move,i=1
Exiting main...
Destructor called.//return 0调用析构函数
Destructor called.//return 0调用析构函数
6.2 指针
6.2.1 内存空间的访问方式
通过变量名或地址访问。
6.2.2 指针变量的声明
指针变量是用于存放内存单元地址的。
指针变量的初始化:
存储类型 数据类型 *指针名=初始地址;
6.2.3 与地址相关的运算“*”和“&”
*在声明语句中表示声明的是指针类型,而在执行语句中表示访问指针对象的内容;
&在声明语句中表示的是声明的是引用类型,而在执行语句中出现在=号后面表示取对象的地址。
6.2.4 指针的赋值
形式:指针名=地址
指针的类型是它所指向变量的类型,而不是指针本身数据值的类型,任何一个指针本身的数据值都是unsigned long int型。
指向常量的指针
int a;
const int *p1 = &a; //p1是指向常量的指针
int b;
p1 = &b; //正确,p1本身的值可以改变
*p1 = 1; //编译时出错,不能通过p1改变所指的对象
指针类型的常量
int a;
int * const p2 = &a;
p2 = &b; //错误,p2是指针常量,值不能改变
6.2.5 指针的运算
p1[n1]等价于*(p1 + n1)。
y=*px++ 相当于 y=*(px++) (*和++优先级相同,自右向左运算)
关系运算
指向相同类型数据的指针之间可以进行关系运算;
指针和0之间可以进行关系运算:p==0或p!=0。
赋值运算
向指针变量赋的值必须是地址常量或变量,不能是普通整数。但可以赋值为整数0,表示空指针,也就是不指向任何有效地址的指针。
6.2.6 用指针处理数组元素
声明与赋值
int a[10], *pa;
pa=&a[0]; 或 pa=a;
经过上述声明及赋值后:
*pa就是a[0],*(pa+1)就是a[1],... ,*(pa+i)就是a[i].
a[i], *(pa+i), *(a+i), pa[i]都是等效的。
不能写 a++,因为a是数组首地址是常量。
6.2.7 指针数组
声明形式:数据类型*数组名[下标表达式];
通过数组元素的地址可以输出二维数组元素,形式如下:
*(*(array+i)+j)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0Jj05hti-1638624426771)(C:\Users\你爸爸\AppData\Roaming\Typora\typora-user-images\image-20211130173422667.png)]
6.2.8 用指针作为函数参数
如果需要传递的数据存放在一个连续的区域里,使用指针作为函数参数只传递数据的起始地址,不用传递数据的值,可以减少开销。
函数设置:
void splitFloat(float x, int *intPart, float *fracPart)
主函数使用:
splitFloat(x, &n, &f); //变量地址作为实参
如果函数体中不需要通过指针改变指针所指对象的内容,应在参数表中将其声明为指向常量的指针,这样是的常对象被取地址后也可以作为该函数的参数。
void print(const int *p, int n);
6.2.9 指针型函数
int* max(int a, int b);
6.2.10 指向函数的指针
声明形式:数据类型 (*函数指针名)(参数表);
#include <iostream>
using namespace std;
void printStuff(float) {
cout << "This is the print stuff function."
<< endl;
}
void printMessage(float data) {
cout << "The data to be listed is "
<< data << endl;
}
void printFloat(float data) {
cout << "The data to be printed is "
<< data << endl;
}
const float PI = 3.14159f;
const float TWO_PI = PI * 2.0f;
int main() { //主函数
void (*functionPointer)(float); //函数指针
printStuff(PI);
functionPointer = printStuff;
*functionPointer(PI); //函数指针调用
functionPointer = printMessage;
functionPointer(TWO_PI); //函数指针调用
functionPointer(13.0); //函数指针调用
functionPointer = printFloat;
functionPointer(PI); //函数指针调用
printFloat(PI);
return 0;
}
6.2.11 对象指针
1.一般对象指针的概念
声明形式 类名 *对象指针名;
访问方法
对象指针名->成员名;
#include <iostream>
using namespace std;
class Point { //类的定义
public: //外部接口
Point(int x = 0, int y = 0) : x(x), y(y) { } //构造函数
int getX() const { return x; } //返回x
int getY() const { return y; } //返回y
private: //私有数据
int x, y;
};
int main() { //主函数
Point a(4, 5); //定义并初始化对象a
Point *p1 = &a; //定义对象指针,用a的地址将其初始化
cout << p1->getX() << endl; //利用指针访问对象成员
cout << a.getX() << endl; //利用对象名访问对象成员
return 0;
}
2.this指针
this指针式隐含于每一个类的非静态成员函数中的特殊指针(包括构造函数和析构函数),它用于指向正在被成员函数操作的对象。
return x;相当于:return this->x;
3.指向类的非静态成员的指针
声明一般形式
类型说明符 类名::*指针名; //声明指向数据成员的指针
类型说明符 (类名::*指针名)(参数表) //声明指向函数成员的指针
赋值一般形式
指针名=&类名::数据成员名; //数据成员指针赋值
指针名=&类名::函数成员名; //函数成员指针赋值
访问类对象成员一般形式
/*访问对象数据成员*/
对象名.*类成员指针名;
或
对象指针名->*类成员指针名
/*访问对象函数成员*/
(对象名.*类成员指针名)(参数表)
或
(对象指针名->*类成员指针名)(参数表)
#include <iostream>
using namespace std;
class Point{
public:
Point(int x = 0, int y = 0):x(x), y(y){}
int getX() const {return x;}
int getY() const {return y;}
private:
int x, y;
};
int main() {
Point a(4, 5);
Point *p1 = &a;
int (Point::*funcPtr)() const = &Point::getX;
cout << (a.*funcPtr)() << endl;
cout << (p1->*funcPtr)() << endl;
cout << a.getX() << endl;
cout << p1->getX() << endl;
return 0;
}
简而言之,就是创建一个指针代理类中的一个成员函数。
4.指向类的静态成员的指针
class Point{
public:
...
static void showCount(){...}
static int count;
};
int Point::count=0;
int main(){
int *ptr=&Point::count;
void (*funcPtr)()=Point::showCount;
...
}
6.3 动态内存分配
6.3.1动态申请内存操作符new和释放内存操作符delete
#include<iostream>
using namespace std;
class Point {...};
int main() {
cout << "Step one: " << endl;
Point *ptr1 = new Point;//调用缺省构造函数
delete ptr1; //删除对象,自动调用析构函数
cout << "Step two: " << endl;
ptr1 = new Point(1,2);
delete ptr1;
return 0;
}
6.3.2申请和释放动态数组
分配:new 类型名T [ 数组长度 ] (数组长度可以是任何表达式,在运行时计算) 释放:delete[] 数组名p (释放指针p所指向的数组。p必须是用new分配得到的数组首地址。)
#include<iostream>
using namespace std;
class Point {...};
int main() {
Point *ptr = new Point[2]; //创建对象数组
ptr[0].move(5, 10); //通过指针访问数组元素的成员
ptr[1].move(15, 20); //通过指针访问数组元素的成员
cout << "Deleting..." << endl;
delete[] ptr; //删除整个对象数组
return 0;
}
运行结果:
Default Constructor called.
Default Constructor called.
Deleting...
Destructor called.
Destructor called.
拓展
用new动态创建一维数组时,在“[]”后还可以加“()”,但"()"内不可以带任何参数。
Point *ptr = new Point[2]; //创建对象数组,数组每个元素的初始化都是执行new Point
cout << ptr[0].getX() << endl;
Point *ptr = new Point[2]();//创建对象数组,数组的初始化都是执行new Point()
cout << ptr[0].getX() << endl;
①若Point的构造函数设置了默认值,则两个输出结果相同。
Point():x(0),y(0){}
②若Point的构造函数没有设置默认值,则两个输出结果都是未定义的数。
Point(){}
所以只要定义了构造函数,无论new的时候后面加不加“()”,都会调用自身已有的构造函数。
③若类中不定义构造函数,而使用编译器的缺省构造函数,则前者输出未定义的数,后者输出0。
④如果是调用内置类型,则和③结果相同
int *a = new int;
cout << a <<endl;//结果为未定义的数
int *b = new int();
cout << b <<endl;//结果为0
6.3.3 动态创建多维数组
char (*fp)[3];
fp = new char[2][3];
6.3.4动态数组类
需知:assert函数(断言)当判断括号内条件表达式值不为true时,程序中止,assert只在调试模式(debug模式)下生效,在发行模式(release模式)不执行任何操作。
#include <iostream>
#include <cassert>
using namespace std;
class Point {...};
class ArrayOfPoints { //动态数组类
public:
ArrayOfPoints(int size) : size(size) {
points = new Point[size];
}
~ArrayOfPoints() {
cout << "Deleting..." << endl;
delete[] points;
}
Point& element(int index) {
//返回“引用”可以用来操作封装数组对象内部的数组元素。
//如果返回“值”则只是返回了一个“副本”,通过“副本”是无法操作原来数组中的元素的
assert(index >= 0 && index < size);
return points[index];
}
private:
Point *points; //指向动态数组首地址
int size; //数组大小
};
int main() {
int count;
cout << "Please enter the count of points: ";
cin >> count;
ArrayOfPoints points(count); //创建对象数组
//通过访问数组元素的成员
points.element(0).move(5, 0);
//通过类访问数组元素的成员
points.element(1).move(15, 20);
return 0;
}
6.4 用vector创建数组对象
vector是C++标准库提供的被封装的动态数组,而且可以具有各种类型。
vector是一个类模板,不是一个类。
使用vector可以很好的检测下标越界的错误。
使用vector定义动态数组的形式
vector <元素类型> 数组对象名(数组长度);
例:
vector<int> arr(5)//建立大小为5的int数组
细节:使用vector定义的数组对象的所有元素都会被初始化,如果元素类型是基本数据类型,则初始化的值都是0;如果元素类型是类类型,则会调用类的默认构造函数进行初始化,因此,使用vector定义类动态数组前,需要保证类具有默认构造函数。
只能设置所有元素为相同初值,形式如下
vector <元素类型> 数组对象名(数组长度,元素初值);
使用vector定义的数组对象名和普通的数组对象名不同的是,使用vector定义的数组对象名表示的是数组对象而不是数组的首地址,因为数组对象不是数组,而是封装了数组的对象。
#include <iostream>
#include <vector>
using namespace std;
//计算数组arr中元素的平均值
double average(const vector<double> &arr) {
double sum = 0;
for (unsigned i = 0; i < arr.size(); i++)
sum += arr[i];
return sum / arr.size();
}//arr.size()返回数组的大小
int main() {
unsigned n;
cout << "n = ";
cin >> n;
vector<double> arr(n); //创建数组对象
cout << "Please input " << n << " real numbers:" << endl;
for (unsigned i = 0; i < n; i++)
cin >> arr[i];
cout << "Average = " << average(arr) << endl;
return 0;
}
声明函数时使用vector定义的数组作为形参形式
void outputVector( const vector <int> & );
void inputVector( vector <int> & );
6.5 深复制和浅复制
深拷贝和浅拷贝最根本的区别在于是否真正获取一个对象的复制实体,而不是引用。
6.5.1 浅复制
浅复制只是创建另外一个指针指向和“复制”的指针相同已存在的地址,并没有创建副本。
#include <iostream>
#include <cassert>
using namespace std;
class Point {...};
class ArrayOfPoints {...};
int main() {
int count;
cout << "Please enter the count of points: ";
cin >> count;
ArrayOfPoints pointsArray1(count); //创建对象数组
pointsArray1.element(0).move(5,10);
pointsArray1.element(1).move(15,20);
ArrayOfPoints pointsArray2(pointsArray1); //创建副本
cout << "Copy of pointsArray1:" << endl;
cout << "Point_0 of array2: " << pointsArray2.element(0).getX() << ", "
<< pointsArray2.element(0).getY() << endl;
cout << "Point_1 of array2: " << pointsArray2.element(1).getX() << ", "
<< pointsArray2.element(1).getY() << endl;
pointsArray1.element(0).move(25, 30);
pointsArray1.element(1).move(35, 40);
cout << "After the moving of pointsArray1:" << endl;
cout << "Point_0 of array2: " << pointsArray2.element(0).getX() << ", "
<< pointsArray2.element(0).getY() << endl;
cout << "Point_1 of array2: " << pointsArray2.element(1).getX() << ", "
<< pointsArray2.element(1).getY() << endl;
return 0;
}
运行结果如下:
Please enter the number of points:2
Default Constructor called.
Default Constructor called.
Copy of pointsArray1:
Point_0 of array2: 5, 10
Point_1 of array2: 15, 20
After the moving of pointsArray1:
Point_0 of array2: 25, 30
Point_1 of array2: 35, 40
Deleting...
Destructor called.
Destructor called.
Deleting...
接下来程序出现异常,也就是运行错误。 原因分析: 在程序结束前,需要调用pointsArray1和pointsArray2的析构函数,但是两个对象共用同一个内存地址,所以该空间被释放两次,导致运行错误。
6.5.2 深复制
深复制创建一个指针,并且申请了新的内存,让这个指针指向新的内存地址,将需要复制的对象的每一个属性都复制了一遍而且两个对象的相同属性使用两个不同的内存地址,也就是说两个对象之间不存在任何一个公用地址。
#include <iostream>
#include <cassert>
using namespace std;
class Point {...};
class ArrayOfPoints {
public:
ArrayOfPoints(const ArrayOfPoints& pointsArray);
...
};
ArrayOfPoints::ArrayOfPoints(const ArrayOfPoints& v) {
size = v.size;
points = new Point[size];
for (int i = 0; i < size; i++)
points[i] = v.points[i];
}
int main() {
//同6.5.1
}
6.6 字符串
6.6.1 用字符数组存储和处理字符串
//以下三条语句具有等价的作用:
char str[8] = { 'p', 'r', 'o', 'g', 'r', 'a', 'm', '\0' };
char str[8] = "program";
char str[] = "program";
用字符数组表示字符串的缺点
① 执行连接、拷贝、比较等操作,都需要显式调用库函数,很麻烦。 ② 当字符串长度很不确定时,需要用new动态创建字符数组,最后要用delete释放,很繁琐。 ③ 字符串实际长度大于为它分配的空间时,会产生数组下标越界的错误。
6.6.2 string类
string类中封装了串的属性并提供了一系列允许访问这些属性的函数。
string();//默认构造函数
string(const string& rhs);//复制构造函数
string(const char*s);//用指针s指向的字符串常量初始化string类的对象
string(const string& rhs,unsigned int pos,unsigned int n);//从rhs的pos位置开始取n个字符,用来初始化string类的对象
string(const char* s,unsigned int n);//用指针s所指向的字符串中的前n个字符初始化string类的对象
string(unsigned int n,char c);//将参数c中的字符重复n次,用来初始化string类的对象
常用成员函数介绍
string getline (cin,s);//输入字符串s
string getline (cin,s,",")//输入字符串s,以,为输入结束的标志
string append (const char* s);//将s添加在本字符串尾
string assign (const char* s);//赋值,将s所指向的字符串赋值给本对象
int compare (const string &str) const;//比较本串与str串的大小,a<str返回负值,a>str返回正值,相等返回0
细节:严格来说,string不是一个独立的类,而是类模板basic_string的一个特化实例。
6.8 深度搜索
第七章 继承与派生
7.1 类的继承与派生
7.1.1 概念
类的继承,是新的类从已有类那里得到已有的特性——从已有类产生新类的过程就是类的派生。
基类或父类(直接基类、间接基类)——派生类或子类。
7.1.2 派生类的定义
继承方式规定了如何访问从基类继承的成员,如果没有给出继承方式关键字,则默认为private。
单继承
class 派生类名:继承方式 基类名{};
多继承
class 派生类名:继承方式1 基类名1,继承方式2 基类名2,...{};
派生类成员指除了从基类继承来的所有成员之外,新增加的数据和函数成员。
7.1.3 派生类生成过程
1.吸收基类成员
派生类包含了它的全部基类中除构造函数和析构函数之外的所有成员。
2.改造基类成员
①基类成员的访问控制问题
主要依靠派生类定义时的继承方式控制。
②对基类数据或函数成员的覆盖或隐藏。
隐藏:也称作同名隐藏,派生类声明一个和某基类成员同名的新成员(成员函数的话参数表也相同,参数不同属于重载),之后在派生类或通过派生类的对象,只能访问到派生类中声明的同名成员,而不能访问到基类的同名成员。
3.添加新的成员
继承与派生机制的核心,保证派生类在功能上有所发展的关键。
7.2 访问控制
1.公有继承
基类的public和protected成员的访问属性在派生类中保持不变,但基类的private成员不可直接访问。
派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。
通过派生类的对象只能访问基类的public成员。
#include <iostream>
using namespace std;
class Point
{
public:
void initPoint(float x=0,float y=0){this->x=x;this->y=y;}//设置x,y值
void movePoint(float offX,float offY){x+=offX;y+=offY;}//移动点函数
float getX(){return x;}
float getY(){return y;}
private:
float x,y;
};
class Rectangle:public Point
{
public:
void initRectangle(float x,float y,float w,float h)
{
initPoint(x,y);
this->w=w;
this->h=h;
}
float getW(){return w;}
float getH(){return h;}
private:
float w,h;
};
int main() {
Rectangle rect; //定义Rectangle类的对象
rect.initRectangle(2, 3, 20, 10); //设置矩形的数据
rect.movePoint(3,2); //移动矩形位置
cout << "The data of rect(x,y,w,h): " << endl;
cout << rect.getX() <<", " //输出矩形的特征参数
<< rect.getY() << ", "
<< rect.getW() << ", "
<< rect.getH() << endl;
return 0;
}
Rectangle类的对象rect可以直接访问基类的公有成员movePoint(…)、getX()、getY()。
2.私有继承
基类的public和protected成员都以private身份出现在派生类中,但基类的private成员不可直接访问。
派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。
通过派生类的对象不能直接访问基类中的任何成员。
#include <iostream>
using namespace std;
class Point
{
public:
void initPoint(float x=0,float y=0){this->x=x;this->y=y;}//设置x,y值
void movePoint(float offX,float offY){x+=offX;y+=offY;}//移动点函数
float getX(){return x;}
float getY(){return y;}
private:
float x,y;
};
class Rectangle:private Point
{
public:
void initRectangle(float x,float y,float w,float h)
{
initPoint(x,y);
this->w=w;
this->h=h;
}
void movePoint(float offX,float offY){Point::movePoint(offX,offY);}//新增共有函数成员调用基类共有成员函数
float getX(){return Point::getX();}//新增共有函数成员调用基类共有成员函数
float getY(){return Point::getY();}//新增共有函数成员调用基类共有成员函数
float getW(){return w;}
float getH(){return h;}
private:
float w,h;
};
int main() {
Rectangle rect; //定义Rectangle类的对象
rect.initRectangle(2, 3, 20, 10); //设置矩形的数据
rect.movePoint(3,2); //移动矩形位置
cout << "The data of rect(x,y,w,h): " << endl;
cout << rect.getX() <<", " //输出矩形的特征参数
<< rect.getY() << ", "
<< rect.getW() << ", "
<< rect.getH() << endl;
return 0;
}
Rectangle类的对象rect不可以直接访问基类的公有成员movePoint(…)、getX()、getY(),如果使用公有继承中的例子,就会出现错误:error: ‘Point’ is not an accessible base of ‘Rectangle’|,所以需要在派生类Rectangle中新增成员函数来调用基类公有成员函数。
3.保护继承
基类的public和protected成员都以protected身份出现在派生类中,但基类的private成员不可直接访问。
派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。
通过派生类的对象不能直接访问基类中的任何成员。
#include <iostream>
using namespace std;
class Point
{
public:
void initPoint(float x=0,float y=0){this->x=x;this->y=y;}//设置x,y值
void movePoint(float offX,float offY){x+=offX;y+=offY;}//移动点函数
float getX(){return x;}
float getY(){return y;}
protected:
float z;
private:
float x,y;
};
class Rectangle:protected Point
{
public:
void initRectangle(float x,float y,float w,float h)
{
initPoint(x,y);
this->w=w;
this->h=h;
z=20.1;
}
void movePoint(float offX,float offY){Point::movePoint(offX,offY);}//移动点函数
float getX(){return Point::getX();}
float getY(){return Point::getY();}
float getZ(){return z;}//直接派生类可以直接访问基类保护成员
float getW(){return w;}
float getH(){return h;}
private:
float w,h;
};
int main() {
Point p;
p.z=2;//编译错误,error: 'float Point::z' is protected within this context|
Rectangle rect; //定义Rectangle类的对象
rect.initRectangle(2, 3, 20, 10); //设置矩形的数据
rect.movePoint(3,2); //移动矩形位置
cout << "The data of rect(x,y,w,h): " << endl;
cout << rect.getX() <<", " //输出矩形的特征参数
<< rect.getY() << ", "
<< rect.getW() << ", "
<< rect.getH() << endl;
cout << "z="<<rect.getZ() << endl;
return 0;
}
4.私有继承和保护继承区别
对派生类来说,在直接派生类中所有成员访问属性和私有继承是一样的。
假设a类的派生类b类有了派生类c类:
当b类私有继承a类,c类的成员和对象都不能访问间接从a类继承的成员;
当b类保护继承a类,那么a类的public和protected成员在b类中都是protected成员,c类有可能访问间接从a类继承来的成员
#include <iostream>
using namespace std;
class Point
{
public:
void initPoint(float x=0,float y=0){this->x=x;this->y=y;}//设置x,y值
void movePoint(float offX,float offY){x+=offX;y+=offY;}//移动点函数
float getX(){return x;}
float getY(){return y;}
protected:
float z;
private:
float x,y;
};
class Rectangle:protected Point
{
public:
void initRectangle(float x,float y,float w,float h)
{
initPoint(x,y);
this->w=w;
this->h=h;
z=20.1;
}
void movePoint(float offX,float offY){Point::movePoint(offX,offY);}//移动点函数
float getX(){return Point::getX();}
float getY(){return Point::getY();}
float getW(){return w;}
float getH(){return h;}
private:
float w,h;
};
class Square:protected Rectangle
{
public:
void initSquare(float x,float y,float w,float h)
{
initRectangle(x,y,w,h);
Rectangle::z=30.1;
}
void movePoint(float offX,float offY){Rectangle::movePoint(offX,offY);}//移动点函数
float getX(){return Point::getX();}//可以访问间接基类Point的非私有成员函数
float getY(){return Point::getY();}//可以访问间接基类Point的非私有成员函数
float getZ(){return z;}//可以访问间接基类Point的非私有数据成员
float getW(){return Rectangle::getW();}//可以访问直接基类Rectangle的非私有成员函数
float getH(){return Rectangle::getH();}
};
int main() {
Square sq; //定义Square类的对象
sq.initSquare(2, 3, 20, 10); //设置矩形的数据
sq.movePoint(3,2); //移动矩形位置
cout << "The data of rect(x,y,w,h): " << endl;
cout << sq.getX() <<", " //输出矩形的特征参数
<< sq.getY() << ", "
<< sq.getW() << ", "
<< sq.getH() << endl;
cout << "z="<<sq.getZ() << endl;
return 0;
}
运行结果:
The data of rect(x,y,w,h):
2,3,20,10
z=30.1
7.3 类型兼容规则
类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代,分为以下情况:
①派生类的对象可以隐含转换为基类对象。
②派生类的对象可以初始化基类的引用。
③派生类的指针可以隐含转换为基类的指针。
在替代之后,派生类对象可以作为基类的对象使用,只能使用从基类继承而来的成员。
#include <iostream>
using namespace std;
class Base1 { //基类Base1定义
public:
void display() const {
cout << "Base1::display()" << endl;}};
class Base2: public Base1 { //公有派生类Base2定义
public:
void display() const {
cout << "Base2::display()" << endl;}};
class Derived: public Base2 { //公有派生类Derived定义
public:
void display() const {
cout << "Derived::display()" << endl;}
};
void fun(Base1 *ptr) { //参数为指向基类对象的指针
ptr->display(); //"对象指针->成员名"}
int main()
{ //主函数
Base1 base1; //声明Base1类对象
Base2 base2; //声明Base2类对象
Derived derived; //声明Derived类对象
fun(&base1); //用Base1对象的指针调用fun函数
fun(&base2); //用Base2对象的指针调用fun函数,在fun函数中转换为Base1类的指针,所以只能使用从Base1类继承的display()
fun(&derived); //用Derived对象的指针调用fun函数,在fun函数中转换为Base1类的指针,所以只能使用从Base1类继承的display()
return 0;
}
运行结果:
Base1::display()
Base1::display()
Base1::display()
7.4 派生类的构造、析构函数
7.4.1 构造函数
在继承时不会继承基类的构造函数,所以就需要在构造派生类的对象时,人为地调用基类的构造函数对于基类的成员对象和新增成员对象初始化。
派生类的构造函数一般语法形式:
派生类名::派生类名(参数表):
基类名1(基类1初始参数表),...,基类名n(基类n初始参数表),
成员对象名1(成员对象1初始化参数表),...,成员对象名n(成员对象n初始化参数表){...}
先调用基类的构造函数,然后调用内嵌对象的构造函数,基类构造函数的调用是按照派生类定义的顺序,调用顺序按照它们被继承时声明的顺序(从左向右),内嵌对象构造函数调用按照在类中声明的先后顺序。
#include <iostream>
using namespace std;
class Base1
{ //基类Base1,构造函数有参数
public:
Base1(int i) { cout << "Constructing Base1 " << i << endl; }};
class Base2
{ //基类Base2,构造函数有参数
public:
Base2(int j) { cout << "Constructing Base2 " << j << endl; }};
class Base3
{ //基类Base3,构造函数无参数
public:
Base3() { cout << "Constructing Base3 *" << endl;
}};
class Derived: public Base2, public Base1, public Base3 {
//派生新类Derived,注意基类名的顺序
public: //派生类的公有成员
Derived(int a, int b, int c, int d): Base1(a), member2(d), member1(c), Base2(b)
{ }
//注意基类名的个数与顺序,//注意成员对象名的个数与顺序
private: //派生类的私有成员对象
Base1 member1;
Base2 member2;
Base3 member3;
};
int main()
{
Derived obj(1, 2, 3, 4);
return 0;
}
运行结果:
constructing Base2 2//Base2(b)
constructing Base1 1//Base1(a)
constructing Base3 *//Base3()
constructing Base1 3//Base1 member1
constructing Base2 4//Base2 member2
constructing Base3 *//Base3 member3
7.4.2 复制构造函数
派生类的复制构造函数会自动调用基类的复制构造函数,声明形式:
Derived::Derived(const Derived &v):Base(v){...}
7.4.3 析构函数
析构函数的执行顺序和构造函数完全相反,先执行析构函数的函数体,然后对派生类新增的类类型的成员对象进行清理,最后对所有从基类继承来的成员进行清理。
#include <iostream>
using namespace std;
class Base1
{ //基类Base1,构造函数有参数
public:
Base1(int i) {this->i=i; cout << "Constructing Base1 " << i << endl; }
~Base1() { cout << "Destructing Base1 " << i <<endl; }
private:
int i;
};
class Base2
{ //基类Base2,构造函数有参数
public:
Base2(int j) {this->j=j; cout << "Constructing Base2 " << j << endl; }
~Base2() { cout << "Destructing Base2 " << j <<endl; }
private:
int j;
};
class Base3
{ //基类Base3,构造函数无参数
public:
Base3() { cout << "Constructing Base3 *" << endl; }
~Base3() { cout << "Destructing Base3-*-*-*-" << endl; }
};
class Derived: public Base2, public Base1, public Base3
{
//派生新类Derived,注意基类名的顺序
public: //派生类的公有成员
Derived(int a, int b, int c, int d): Base1(a), member2(d), member1(c), Base2(b) { }
//注意基类名的个数与顺序,注意成员对象名的个数与顺序
~Derived(){cout << "Destructing Derived" << endl;}
private: //派生类的私有成员对象
Base1 member1;
Base2 member2;
Base3 member3;
};
int main()
{
Derived obj(1, 2, 3, 4);
return 0;
}
运行结果:
constructing Base2 2 //Base2(b)
constructing Base1 1 //Base1(a)
constructing Base3 * //Base3()
constructing Base1 3 //Base1 member1(c)
constructing Base2 4 //Base2 member2(d)
constructing Base3 * //Base3 member3
Destructing Derived //执行派生类析构函数的函数体
Destructing Base3-*-*-*- //清理派生类新增的类类型的成员对象member3.~Base3()
Destructing Base2 4 //清理派生类新增的类类型的成员对象member2.~Base2()
Destructing Base1 3 //清理派生类新增的类类型的成员对象member1.~Base1()
Destructing Base3-*-*-*- //清理从基类继承来的成员Base3.~Base3()
Destructing Base1 1 //清理从基类继承来的成员Base1(1).~Base1()
Destructing Base2 2 //清理从基类继承来的成员Base2(2).~Base2()
7.5 派生类成员的标识与访问
7.5.1 作用域分辨符(::)
如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数表不同,从基类继承的同名函数的所有重载形式也都会被隐藏,如果要访问被隐藏的成员,就需要使用作用域分辨符和基类名来限定。
在多继承中,如果派生类没有声明同名成员,且各基类中存在相同名称的成员,则派生类对象名.成员名或派生类对象指针->成员名,会产生二义性。
细节:派生类中定义了基类同名成员,但是参数表不同的话,不属于函数重载,但是调用该基类成员也需要基类名::成员名。
#include <iostream>
using namespace std;
class Base1
{ //定义基类Base1
public:
int var;
void fun() { cout << "Member of Base1" << endl; }
};
class Base2
{ //定义基类Base2
public:
int var;
void fun() { cout << "Member of Base2" << endl; }
};
class Derived: public Base1, public Base2
{ //定义派生类Derived
public:
int var; //同名数据成员
};
int main()
{
Derived d;
Derived *p = &d;
d.var = 1; //对象名.成员名标识
//d.fun(); //编译错误,
d.Base1::var = 2; //作用域操作符标识
d.Base1::fun(); //访问Base1基类成员
p->Base2::var = 3; //作用域操作符标识
p->Base2::fun(); //访问Base2基类成员
return 0;
}
运行结果:
Member of Base1
Member of Base2
在Derived类中新增两个成员函数,一个解决二义性,一个与基类同名但参数表不同,如下
#include <iostream>
using namespace std;
class Base1
{ //定义基类Base1
public:
int var;
void fun() { cout << "Member of Base1" << endl; }
};
class Base2
{ //定义基类Base2
public:
int var;
void fun() { cout << "Member of Base2" << endl; }
};
class Derived: public Base1, public Base2
{ //定义派生类Derived
public:
int var; //同名数据成员
void fun(){Base1::fun();}//或using Base1::fun;
void fun(int i){cout << "Member of Derived" << endl;}
};
int main()
{
Derived d;
Derived *p = &d;
d.var = 1; //对象名.成员名标识
d.fun(1); //访问Derived类成员
d.fun(); //通过访问Derived类成员,访问Base1基类成员
d.Base1::var = 2; //作用域操作符标识
d.Base1::fun(); //访问Base1基类成员
p->Base2::var = 3; //作用域操作符标识
p->Base2::fun(); //访问Base2基类成员
return 0;
}
运行结果:
Member of Derived
Member of Base1
Member of Base1
Member of Base2
多继承同名隐藏实例
#include <iostream>
using namespace std;
class Base0 { //定义基类Base0
public:int var0;
void fun0() { cout << "Member of Base0" << endl; }};
class Base1: public Base0 { //定义派生类Base1
public: //新增外部接口
int var1;};
class Base2: public Base0 { //定义派生类Base2
public: //新增外部接口
int var2;};
class Derived: public Base1, public Base2 {//定义派生类Derived
public: //新增外部接口
int var;
void fun() { cout << "Member of Derived" << endl; }};
?
int main() { //程序主函数
Derived d; //定义Derived类对象d
d.Base1::var0 = 2; //使用直接基类
d.Base1::fun0();
d.Base2::var0 = 3; //使用直接基类
d.Base2::fun0();
return 0;
}
运行结果:
Member of Base0
Member of Base0
上述派生类对象在内存中拥有两份var0的副本,可以存放不同的值,可以通过Base1和Base2调用Base0的构造函数初始化,或者使用作用域分辨符通过直接基类名限定类分别访问。
上述派生类对象在内存中始终只有一份fun0的副本,之所以需要通过基类名Base1或Base2加以限定,是因为调用非静态成员函数总是针对特定的对象,执行函数调用时需要将指向该类的一个对象的指针作为隐含的参数传递给被调函数来初始化this指针,上述例子中,因为有两个Base0类的子对象,所以作用域分辨符用Base1或Base2限定,哪个派生类的Base0子对象被调用。
7.5.2 虚基类
为了解决上面的问题,我们将共同基类设置为虚基类,这时从不同路径继承过来的同名数据成员在内存中就只有一个副本,同一个函数名也只有一个映射。
虚基类的声明式在派生类的定义过程中进行的,语法形式为
class 派生类名:virtual 继承方式 基类名
#include <iostream>
using namespace std;
class Base0 { //定义基类Base0
public:int var0;
void fun0() { cout << "Member of Base0" << endl; }};
class Base1: virtual public Base0 { //定义派生类Base1
public: //新增外部接口
int var1;};
class Base2: virtual public Base0 { //定义派生类Base2
public: //新增外部接口
int var2;};
class Derived: public Base1, public Base2 {//定义派生类Derived
public: //新增外部接口
int var;
void fun() { cout << "Member of Derived" << endl; }};
?
int main() { //程序主函数
Derived d; //定义Derived类对象d
d.var0 = 2; //直接访问虚基类的数据成员
d.fun0(); //直接访问虚基类的函数成员
return 0;
}
运行结果:
Member of Base0
不使用虚基类可以容纳更多数据,使用虚基类更简洁,内存空间更节省。
7.5.3 虚基类及其派生类构造函数
#include <iostream>
using namespace std;
class Base0 { //定义基类Base0
public:
Base0(int var) : var0(var) { }
int var0;
void fun0() { cout << "Member of Base0" << endl; }};
class Base1: virtual public Base0 {//定义派生类Base1
public: //新增外部接口
Base1(int var) : Base0(var) { }
int var1;};
class Base2: virtual public Base0 {//定义派生类Base2
public: //新增外部接口
Base2(int var) : Base0(var) { }
int var2;};
class Derived: public Base1, public Base2 {
//定义派生类Derived
public: //新增外部接口
Derived(int var) : Base0(var), Base1(var), Base2(var) { }
int var;
void fun() { cout << "Member of Derived" << endl; }};
int main() { //程序主函数
Derived d(1); //定义Derived类对象d
d.var0 = 2; //直接访问虚基类的数据成员
d.fun0(); //直接访问虚基类的函数成员
return 0;
}
单看代码,建立Derived类对象d时需要调用Base0构造函数并初始化var0、Base1和Base2构造函数Base1()和Base2(),那么岂不是要初始化3次从虚基类继承来的var0。
对此C++给出了解决方法,将建立对象时所指定的类称为当时的最远派生类,当此类对象含有从虚基类继承来的成员,则只由最远派生类的构造函数调用虚基类的构造函数进行初始化,而忽略其他类对虚基类构造函数的调用。
拓展
构造一个类的对象的一般顺序是:
- 如果有直接或间接的虚基类,则先执行虚基类的构造函数;
- 按照继承顺序执行各非虚基类构造函数,但不再执行它们的虚基类的构造函数;
- 按照类定义的新增成员顺序执行类构造函数;
- 执行构造函数的函数体。
7.8 深度探索
第八章 多态性
8.1 多态性概述
多态是指同样的消息被不同类型的对象接收时导致不同的行为。
8.1.1 多态的类型
面向对象的多态性分为四种类型:
专用多态:重载多态、强制多态。
通用多态:包含多态、参数多态。
根据多态性作用的时机可以分为:编译时的多态、运行时的多态。
8.1.2 多态的实现
绑定是指将一个标识符名和一个存储地址联系在一起的过程。
编译时的多态——绑定工作在编译连接阶段完成的情况称为静态绑定。
运行时的多态——绑定工作在程序运行阶段完成的情况称为动态绑定
8.2 运算符重载
运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用与不同类型的数据时导致不同的行为。
运算符重载的本质就是函数重载。
8.2.1 运算符重载的规则
1.只能重载C++中已经有的运算符
不能重载的运算符举例:类属关系运算符“.”、成员指针运算符“.*”、作用域分辨符“::”、三目运算符“?:”
2.重载之后运算符的优先级和结合性都不会改变
3.运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造
两种重载方式
重载为类的非静态成员函数和重载为非成员函数
声明形式
函数类型 operator 运算符(形参)
{
......
}
当运算符重载为成员函数的时候,函数的参数个数要比原操作个数少一个(后置“++”,“–”除外),因为第一个操作数会被作为函数调用的目的对象,因此无需出现在参数表中,函数体中可以直接访问第一个操作数的成员;
当运算符重载为非成员函数的时候,函数的参数个数和原操作个数相同,且至少应该有一个自定义类型的形参,因为运算符的所有操作数必须显式通过参数传递。
8.2.2 运算符重载为成员函数
双目运算符 B
如果要重载 B 为类成员函数,使之能够实现表达式 oprd1 B oprd2,其中 oprd1 为A 类对象,则 B 应被重载为 A 类的成员函数,形参类型应该是 oprd2 所属的类型。 经重载后,表达式 oprd1 B oprd2 相当于 oprd1.operator B(oprd2)。
#include <iostream>
using namespace std;
class Complex { //复数类定义
public: //外部接口
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) { } //构造函数
Complex operator + (const Complex &c2) const; //运算符+重载成员函数
Complex operator - (const Complex &c2) const; //运算符-重载成员函数
void display() const; //输出复数
private: //私有数据成员
double real; //复数实部
double imag; //复数虚部
};
Complex Complex::operator + (const Complex &c2) const { //重载运算符函数实现
return Complex(real + c2.real, imag + c2.imag); //创建一个临时无名对象作为返回值
}
Complex Complex::operator - (const Complex &c2) const { //重载运算符函数实现
return Complex(real - c2.real, imag - c2.imag); //创建一个临时无名对象作为返回值
}
void Complex::display() const {
cout << "(" << real << ", " << imag << ")" << endl;
}
int main() { //主函数
Complex c1(5, 4), c2(2, 10), c3; //定义复数类的对象
cout << "c1 = "; c1.display();
cout << "c2 = "; c2.display();
c3 = c1 - c2; //使用重载运算符完成复数减法
cout << "c3 = c1 - c2 = "; c3.display();
c3 = c1 + c2; //使用重载运算符完成复数加法
cout << "c3 = c1 + c2 = "; c3.display();
return 0;
}
程序输出的结果为:
c1 = (5, 4)
c2 = (2, 10)
c3 = c1 - c2 = (3, -6)
c3 = c1 + c2 = (7, 14)
前置单目运算符U
如果要重载为类的成员函数,使之能够实现表达式U oprd,其中oprd为A类的对象,则U应当重载为A类的成员函数,函数没有形参。
经重载后,表达式U oprd相当于oprd.operator U()。
后置单目运算符++、–
如果要重载为类的成员函数,使之能够实现表达式oprd++或oprd–,其中oprd为A类的对象,则U应当重载为A类的成员函数,函数要带有一个整型int形参。
经重载后,表达式oprd++和oprd–相当于oprd.operator++(0)和oprd.operator–(0)。
#include <iostream>
using namespace std;
class Clock { //时钟类定义
public: //外部接口
Clock(int hour = 0, int minute = 0, int second = 0);
void showTime() const;
Clock& operator ++ (); //前置单目运算符重载
Clock operator ++ (int); //后置单目运算符重载
private: //私有数据成员
int hour, minute, second;
};
?
Clock::Clock(int hour/* = 0 */, int minute/* = 0 */, int second/* = 0 */) {
if (0 <= hour && hour < 24 && 0 <= minute && minute < 60
&& 0 <= second && second < 60) {
this->hour = hour;
this->minute = minute;
this->second = second;
} else
cout << "Time error!" << endl;
}
void Clock::showTime() const { //显示时间函数
cout << hour << ":" << minute << ":" << second << endl;
}
?
Clock & Clock::operator ++ () { //前置单目运算符重载函数
second++;
if (second >= 60) {
second -= 60;
minute++;
if (minute >= 60) {
minute -= 60;
hour = (hour + 1) % 24;
}
}
return *this;
}
?
Clock Clock::operator ++ (int) { //后置单目运算符重载
//注意形参表中的整型参数
Clock old = *this;
++(*this); //调用前置“++”运算符
return old;
}
int main() {
Clock myClock(23, 59, 59);
cout << "First time output: ";
myClock.showTime();
cout << "Show myClock++: ";
(myClock++).showTime();
cout << "Show ++myClock: ";
(++myClock).showTime();
return 0;
}
运行结果:
First time output: 23:59:59
Show myClock++: 23:59:59
Show ++myClock: 0:0:1
前置单目运算符和后置单目运算符最主要的区别就在于重载函数的形参。
(对于函数参数表中并未使用的参数,C++允许不给出参数名,所有后置单目运算符重载的int形参可以只给出类型名)
8.2.3 运算符重载为非成员函数
函数的形参代表依自左至右次序排列的各操作数。
如果在运算符的重载函数中需要操作某类对象的私有成员或保护成员,可以将此函数声明为该类的友元,但不要盲目声明为类的友元函数。
如果不声明为友元函数,则该函数仅依赖于类的接口,只要类不变,函数的实现就不需要变;如果声明为友元函数,该函数就依赖于类的实现,即使类的接口不变,只要类的私有数据成员发生变化,该函数的实现就必须变化。
双目运算符 B
重载后,表达式oprd1 B oprd2 等同于operator B(oprd1,oprd2 )
//8_3.cpp
#include <iostream>
using namespace std;
class Complex { //复数类定义
public: //外部接口
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) { } //构造函数
friend Complex operator + (const Complex &c1, const Complex &c2); //运算符+重载
friend Complex operator - (const Complex &c1, const Complex &c2); //运算符-重载
friend ostream & operator << (ostream &out, const Complex &c); //运算符<<重载
private: //私有数据成员
double real;//复数实部
double imag;//复数虚部
};
Complex operator + (const Complex &c1, const Complex &c2) { //重载运算符函数实现
return Complex(c1.real + c2.real, c1.imag + c2.imag);
}
Complex operator - (const Complex &c1, const Complex &c2) { //重载运算符函数实现
return Complex(c1.real - c2.real, c1.imag - c2.imag);
}
ostream & operator << (ostream &out, const Complex &c) { //重载运算符函数实现
out << "(" << c.real << ", " << c.imag << ")";
return out;
}
int main() { //主函数
Complex c1(5, 4), c2(2, 10), c3; //定义复数类的对象
cout << "c1 = " << c1 << endl;
cout << "c2 = " << c2 << endl;
c3 = c1 - c2 - c2; //使用重载运算符完成复数减法
cout << "c3 = c1 - c2 - c2 = " << c3 << endl;
c3 = 5.0 + c2; //使用重载运算符完成复数加法
cout << "c3 = 5.0 + c2 = " << c3 << endl;
return 0;
}
程序输出的结果为:
c1 = (5, 4)
c2 = (2, 10)
c3 = c1 - c2 - c2 = (1, -16)
c3 = 5.0 + c2 = (7, 10)
“<<”操作符的左操作数是ostream类型的引用,ostream是cout的一个基类,右操作数是Complex类型的引用,在执行cout<<c1时,调用operator<<(cout,c1)。该函数把第一个参数传入的ostream对象以引用形式返回,是为了支持形如“cout<<c1<<c2”的连续输出,因为第二个“<<”操作符的左操作数是第一个“<<”操作符的返回结果,所以需要返回引用的形式。
前置单目运算符 U
重载后,表达式 U oprd 等同于operator U(oprd )
后置单目运算符 ++和–
重载后,表达式 oprd B 等同于operator B(oprd,0 )
8.3 虚函数
虚函数是动态绑定的基础;虚函数必须是非静态的成员函数;虚函数经过派生之后,在类族中就可以实现运行过程中的多态。
8.3.1 一般虚函数成员
声明形式
virtual 函数类型 函数名(形参表)
{
...
}
虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候。
运行过程中的多态必须满足条件
-
类之间满足赋值兼容规则。 赋值兼容规则简单分类:派生类的对象可以赋值给基类对象、派生类的对象可以初始化基类的引用、派生类对象的地址可以赋给指向基类的指针。 -
声明虚函数 -
由成员函数来调用或者是通过指针、引用来访问虚函数(如果是使用对象名来访问虚函数,在编译过程中就可以进行(静态绑定))
虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态的,所以一般虚函数不能以内联函数处理,但如果声明为内联函数也不会引起错误。
#include <iostream>
using namespace std;
class Base1 { //基类Base1定义
public:
virtual void display() const; //虚函数
};
void Base1::display() const {
cout << "Base1::display()" << endl;
}
class Base2:public Base1 { //公有派生类Base2定义
public:
/*virtual*/void display() const; //覆盖基类的虚函数
};
void Base2::display() const {
cout << "Base2::display()" << endl;
}
class Derived: public Base2 { //公有派生类
public:
/*virtual*/void display() const; //覆盖基类的虚函数
};
void Derived::display() const {
cout << "Derived::display()" << endl;
}
?
void fun(Base1 *ptr) { //参数为指向基类对象的指针
ptr->display(); //"对象指针->成员名"
}
?
int main() { //主函数
Base1 base1; //定义Base1类对象
Base2 base2; //定义Base2类对象
Derived derived; //定义Derived类对象
fun(&base1);//用Base1对象的指针调用fun函数
fun(&base2);//用Base2对象的指针调用fun函数
fun(&derived);//用Derived对象的指针调用fun函数
return 0;
}
运行结果:
Base1::display()
Base2::display()
Derived::display()
上述程序满足运行中的多态必需三个条件:Base1、Base2和Derived属于同一个类族,通过公有派生而来,满足赋值兼容规则;Base1中声明了虚函数;程序中通过对象指针调用成员。
当派生类没有显式声明虚函数时,系统遵循一下规则判断:
- 是否与基类虚函数有相同的名称
- 是否与基类虚函数有相同的参数个数和类型
- 是否与基类虚函数有相同的返回值或满足赋值兼容规则的指针、引用型
满足以上三个条件,会自动确定为虚函数,这时派生类虚函数会覆盖基类虚函数,同时隐藏基类中同名函数的所有重载形式。
补充:如果fun函数换成以下形式,会有不同结果
//情况1
void fun(Base1 *ptr) { //参数为指向基类对象的指针
ptr->Base1::display(); //"对象指针->基类::成员名"调用被覆盖的基类成员函数
}
//情况2
void fun(Base1 ptr) { //参数为基类对象
ptr.display(); //"对象名.成员名"调用成员函数
}
int main() {
fun(base1);//用Base1对象调用fun函数
fun(base2);//用Base2对象调用fun函数
fun(derived);//用Derived对象调用fun函数
}
运行结果都为:
Base1::display()
Base1::display()
Base1::display()
补充
对象切片:用派生类对象复制构造基类对象的行为。例如
Derived d;
Base1 b = d;//调用Base1的复制构造函数用d构造b
8.3.2 虚析构函数
C++中不可以声明虚构造函数,但可以声明虚析构函数,如果类的析构函数是虚函数,那么它的所有派生类的析构函数也是虚函数。
简单来说,如果有可能通过基类指针删除派生类对象,就需要让析构函数是虚函数,否则会产生不确定的结果。
声明形式
virtual ~类名();
不声明析构函数为虚函数
#include <iostream>
using namespace std;
?
class Base {
public:
~Base();
};
Base::~Base() {
cout<< "Base destructor" << endl;
}
?
class Derived: public Base{
public:
Derived();
~Derived();
private:
int *p;
};
Derived::Derived() {
p = new int(0);
}
Derived::~Derived() {
cout << "Derived destructor" << endl;
delete p;
}
?
void fun(Base* b) {
delete b;
}
?
int main() {
Base *b = new Derived();
fun(b);
return 0;
}
运行结果:
Base destructor
此时派生类析构函数不执行,内存泄漏。
析构函数声明为虚函数
#include <iostream>
using namespace std;
?
class Base {
public:
virtual ~Base();
};
Base::~Base() {
cout<< "Base destructor" << endl;
}
?
class Derived: public Base{
public:
Derived();
~Derived();
private:
int *p;
};
Derived::Derived() {
p = new int(0);
}
Derived::~Derived() {
cout << "Derived destructor" << endl;
delete p;
}
?
void fun(Base* b) {
delete b;
}
?
int main() {
Base *b = new Derived();
fun(b);
return 0;
}
运行结果:
Derived destructor
Base destructor
内存空间被正确释放,实现了多态。
8.4 纯虚函数与抽象类
抽象类为类族提供统一的操作界面,可以说,建立抽象类就是为了通过它多态地使用其中的成员函数。
抽象类是带纯虚函数的类,抽象类不能实例化。
声明形式
virtual 函数类型 函数名(参数表) = 0;
声明为纯虚函数之后,基类中就可以不再给出函数的实现部分,函数体由派生类给出。
//8_6.cpp
#include <iostream>
using namespace std;
?
class Base1 { //基类Base1定义
public:
virtual void display() const = 0; //纯虚函数
};
?
class Base2: public Base1 { //公有派生类Base2定义
public:
void display() const; //覆盖基类的虚函数
};
void Base2::display() const {
cout << "Base2::display()" << endl;
}
class Derived: public Base2 { //公有派生类Derived定义
public:
void display() const; //覆盖基类的虚函数
};
void Derived::display() const {
cout << "Derived::display()" << endl;
}
?
void fun(Base1 *ptr) { //参数为指向基类对象的指针
ptr->display(); //"对象指针->成员名"
}
?
int main() { //主函数
Base2 base2; //定义Base2类对象
Derived derived; //定义Derived类对象
fun(&base2); //用Base2对象的指针调用fun函数
fun(&derived); //用Derived对象的指针调用fun函数
return 0;
}
运行结果:
Base2::display()
Derived::display()
8.7 深度探索
第九章 群体类和群体数据的组织
9.1 函数模板与类模板
参数化多态性:将程序所处理的对象的类型参数化,使得一段程序可以用于处理多种不同类型的对象。
9.1.1 函数模板
定义形式
template<模板参数表>//模板参数表多为typename T或class T
类型名 函数名(参数表)
{
...
}
例:求绝对值函数的模板
#include <iostream>
using namespace std;
template<typename T>
T abs(T x) {
return x < 0? -x : x;
}
int main() {
int n = -5;
double d = -5.5;
cout << abs(n) << endl;//abs(n)会让编译器生成函数模板的一个实例int abs(int x),并传入n的值执行此函数
cout << abs(d) << endl;//abs(b)会让编译器生成函数模板的一个实例double abs(double x),并传入b的值执行此函数
return 0;
}
例
#include <iostream>
using namespace std;
?
template <class T> //定义函数模板
void outputArray(const T *array, int count) {
for (int i = 0; i < count; i++)
cout << array[i] << " ";
cout << endl;
}
?
int main() { //主函数
const int A_COUNT = 8, B_COUNT = 8, C_COUNT = 20;
int a [A_COUNT] = { 1, 2, 3, 4, 5, 6, 7, 8 }; //定义int数组
double b[B_COUNT] = { 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8 };//定义double数组
char c[C_COUNT] = "Welcome to see you!";//定义char数组
?
cout << " a array contains:" << endl;
outputArray(a, A_COUNT); //调用函数模板
cout << " b array contains:" << endl;
outputArray(b, B_COUNT); //调用函数模板
cout << " c array contains:" << endl;
outputArray(c, C_COUNT); //调用函数模板
return 0;
}
运行结果:
a array contains:
1 2 3 4 5 6 7 8
b array contains:
1.1 2.2 3.3 4.4 5.5 6.6 7.7 8.8
c array contains:
W e l c o m e t o s e e y o u !
注意
- 函数模板编译时不会生成任何目标代码,只有实例会生成;
- 被多个源文件引用的模板应该把函数体也放进头文件,而不能像普通函数那样只放声明;
- 函数指针只能指向实例,不可以指向模板本身。
9.1.2 类模板
使用类模板使用户可以为类声明一种模式,使得类中的某些数据成员、某些成员函数的参数、某些成员函数的返回值,能取任意类型(包括基本类型的和用户自定义类型)。
语法形式:
template<模板参数表>
class 类名
{
...
}
//在类模板外定义成员函数:
template<模板参数表>
类型名 类名<模板参数标识符列表>::函数名(参数表)
//使用模板类建立对象
模板名<模板参数表>对象名1,...,对象名n
例
#include <iostream>
#include <cstdlib>
using namespace std;
struct Student {
int id; //学号
float gpa; //平均分
};
template <class T>
class Store {//类模板:实现对任意类型数据进行存取
private:
T item; // item用于存放任意类型的数据
bool haveValue; // haveValue标记item是否已被存入内容
public:
Store(); // 缺省形式(无形参)的构造函数
T &getElem(); //提取数据函数
void putElem(const T &x); //存入数据函数
};
template <class T> //默认构造函数的实现
Store<T>::Store(): haveValue(false) { }
template <class T> //提取数据函数的实现
T &Store<T>::getElem() {
//如试图提取未初始化的数据,则终止程序
if (!haveValue) {
cout << "No item present!" << endl;
exit(1); //使程序完全退出,返回到操作系统。
}
return item; // 返回item中存放的数据
}
template <class T> //存入数据函数的实现
void Store<T>::putElem(const T &x) {
// 将haveValue 置为true,表示item中已存入数值
haveValue = true;
item = x; // 将x值存入item
}
int main() {
Store<int> s1, s2;
s1.putElem(3);
s2.putElem(-7);
cout << s1.getElem() << " " << s2.getElem() << endl;
Student g = { 1000, 23 };
Store<Student> s3;
s3.putElem(g);
cout << "The student id is " << s3.getElem().id << endl;
Store<double> d;
cout << "Retrieving object D... ";
cout << d.getElem() << endl
//由于d未经初始化,在执行函数D.getElement()过程中导致程序终止
return 0;
}
运行结果:
3 -7
The student id is 1000
Retrieving object D... No item present!
9.2 线性群体
9.2.1 线性群体的概念
群体是指由多个数据元素组成的集合体。群体可以分为两个大类:线性群体和非线性群体。
线性群体中的元素次序与其位置关系是对应的。在线性群体中,又可按照访问元素的不同方法分为直接访问、顺序访问和索引访问。
对可直接访问的线性群体,我们可以直接访问群体中的任何一个元素,例如通过数组下标直接访问数组元素。对顺序访问的线性群体,只能按照元素的排列顺序从头开始访问各个元素。
9.2.2 直接访问群体——数组类
静态数组缺点:大小在编译时就已经确定,在运行时无法修改。
动态数组优点:其元素个数可在程序运行时改变。
vector就是用类模板实现的动态数组。
动态数组类模板 Array,声明和实现都在Array.h文件中
#ifndef ARRAY_H
#define ARRAY_H
#include <cassert>
template <class T> //数组类模板定义
class Array {
private:
T* list; //用于存放动态分配的数组内存首地址
int size; //数组大小(元素个数)
public:
Array(int sz = 50); //构造函数
Array(const Array<T> &a); //拷贝构造函数
~Array(); //析构函数
Array<T> & operator = (const Array<T> &rhs); //重载"=“
T & operator [] (int i); //重载"[]”
const T & operator [] (int i) const;
operator T * (); //重载到T*类型的转换
operator const T * () const;
int getSize() const; //取数组的大小
void resize(int sz); //修改数组的大小
};
template <class T> Array<T>::Array(int sz) {//构造函数
assert(sz >= 0);//sz为数组大小(元素个数),应当非负
size = sz; // 将元素个数赋值给变量size
list = new T [size]; //动态分配size个T类型的元素空间
}
template <class T> Array<T>::~Array() { //析构函数
delete [] list;
}
//拷贝构造函数
template <class T> Array<T>::Array(const Array<T> &a) {
size = a.size; //从对象x取得数组大小,并赋值给当前对象的成员
//为对象申请内存并进行出错检查
list = new T[size]; // 动态分配n个T类型的元素空间
for (int i = 0; i < size; i++) //从对象X复制数组元素到本对象
list[i] = a.list[i];
}
//重载"="运算符,将对象rhs赋值给本对象。实现对象之间的整体赋值
template <class T>
Array<T> &Array<T>::operator = (const Array<T>& rhs) {
if (&rhs != this) {
//如果本对象中数组大小与rhs不同,则删除数组原有内存,然后重新分配
if (size != rhs.size) {
delete [] list; //删除数组原有内存
size = rhs.size; //设置本对象的数组大小
list = new T[size]; //重新分配n个元素的内存
}
//从对象X复制数组元素到本对象
for (int i = 0; i < size; i++)
list[i] = rhs.list[i];
}
return *this; //返回当前对象的引用
}
//重载下标运算符,实现与普通数组一样通过下标访问元素,并且具有越界检查功能
template <class T>
T &Array<T>::operator[] (int n) {
assert(n >= 0 && n < size); //检查下标是否越界
return list[n]; //返回下标为n的数组元素
}
template <class T>
const T &Array<T>::operator[] (int n) const {
assert(n >= 0 && n < size); //检查下标是否越界
return list[n]; //返回下标为n的数组元素
}?
//重载指针转换运算符,将Array类的对象名转换为T类型的指针
template <class T>
Array<T>::operator T * () {
return list; //返回当前对象中私有数组的首地址
}
?
template <class T>
Array<T>::operator const T * () const {
return list; //返回当前对象中私有数组的首地址
}
//取当前数组的大小
template <class T>
int Array<T>::getSize() const {
return size;
}
// 将数组大小修改为sz
template <class T>
void Array<T>::resize(int sz) {
assert(sz >= 0); //检查sz是否非负
if (sz == size) //如果指定的大小与原有大小一样,什么也不做
return;
T* newList = new T [sz]; //申请新的数组内存
int n = (sz < size) ? sz : size;//将sz与size中较小的一个赋值给n
//将原有数组中前n个元素复制到新数组中
for (int i = 0; i < n; i++)
newList[i] = list[i];
delete[] list; //删除原数组
list = newList; // 使list指向新数组
size = sz; //更新size
}
#endif //ARRAY_H
浅复制和深复制
如果没有对“=”重载,那么系统自动生成隐含的重载函数,给每个数据成员执行“=”运算符,也就是浅复制。所以当需要通过显式定义复制构造函数执行深复制的时候,就需要重载赋值运算符。
与众不同的运算符
重载的“=”和“[]”返回值类型都是对象的引用,因为“=”左值不能为一个对象的值,对其赋值没有意义,而“[]”经常需要作为左值,所以将“[]”返回值类型指定为对象的引用,通过引用可以改变对象的值。
指针转换运算符的作用
#include <iostream>
using namespace std;
void read(int *p, int n) {
for (int i = 0; i < n; i++)
cin >> p[i];
}
int main() {
int a[10];
read(a, 10);
return 0;
}
#include "Array.h"
#include <iostream>
using namespace std;
void read(int *p, int n) {
for (int i = 0; i < n; i++)
cin >> p[i];
}
int main() {
Array<int> a(10);
read(a, 10);
return 0;
}
调用read函数的时候系统会尝试把对象名转换为int*类型,会编译错误,所以需要定义指针类型转换函数。
//重载指针转换运算符,将Array类的对象名转换为T类型的指针
template <class T>
Array<T>::operator T * () {
return list; //返回当前对象中私有数组的首地址
}
Array类的应用
求范围2~N中的质数,N在程序运行时由键盘输入。
#include <iostream>
#include <iomanip>
#include "Array.h"
using namespace std;
int main() {
Array<int> a(10); // 用来存放质数的数组,初始状态有10个元素。
int n, count = 0;
cout << "Enter a value >= 2 as upper limit for prime numbers: ";
cin >> n;
for (int i = 2; i <= n; i++) {
bool isPrime = true;
for (int j = 0; j < count; j++)
if (i % a[j] == 0) { //若i被a[j]整除,说明i不是质数
isPrime = false; break;
}
if (isPrime) { //如果质数表填满了,将其空间加倍
if (count == a.getSize()) a.resize(count * 2);
a[count++] = i;
}
}
for (int i = 0; i < count; i++) cout << setw(8) << a[i];
cout << endl;
return 0;
}
运行结果:
Enter a value >= 2 as upper limit for prime numbers:100
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
现对象之间的整体赋值 template Array &Array::operator = (const Array& rhs) { if (&rhs != this) { //如果本对象中数组大小与rhs不同,则删除数组原有内存,然后重新分配 if (size != rhs.size) { delete [] list; //删除数组原有内存 size = rhs.size; //设置本对象的数组大小 list = new T[size]; //重新分配n个元素的内存 } //从对象X复制数组元素到本对象 for (int i = 0; i < size; i++) list[i] = rhs.list[i]; } return this; //返回当前对象的引用 } //重载下标运算符,实现与普通数组一样通过下标访问元素,并且具有越界检查功能 template T &Array::operator[] (int n) { assert(n >= 0 && n < size); //检查下标是否越界 return list[n]; //返回下标为n的数组元素 } template const T &Array::operator[] (int n) const { assert(n >= 0 && n < size); //检查下标是否越界 return list[n]; //返回下标为n的数组元素 }? //重载指针转换运算符,将Array类的对象名转换为T类型的指针 template Array::operator T * () { return list; //返回当前对象中私有数组的首地址 } ? template Array::operator const T * () const { return list; //返回当前对象中私有数组的首地址 } //取当前数组的大小 template int Array::getSize() const { return size; } // 将数组大小修改为sz template void Array::resize(int sz) { assert(sz >= 0); //检查sz是否非负 if (sz == size) //如果指定的大小与原有大小一样,什么也不做 return; T newList = new T [sz]; //申请新的数组内存 int n = (sz < size) ? sz : size;//将sz与size中较小的一个赋值给n //将原有数组中前n个元素复制到新数组中 for (int i = 0; i < n; i++) newList[i] = list[i]; delete[] list; //删除原数组 list = newList; // 使list指向新数组 size = sz; //更新size } #endif //ARRAY_H
##### 浅复制和深复制
如果没有对“=”重载,那么系统自动生成隐含的重载函数,给每个数据成员执行“=”运算符,也就是浅复制。所以当需要通过显式定义复制构造函数执行深复制的时候,就需要重载赋值运算符。
##### 与众不同的运算符
重载的“=”和“[]”返回值类型都是对象的引用,因为“=”左值不能为一个对象的值,对其赋值没有意义,而“[]”经常需要作为左值,所以将“[]”返回值类型指定为对象的引用,通过引用可以改变对象的值。
##### 指针转换运算符的作用
```c++
#include <iostream>
using namespace std;
void read(int *p, int n) {
for (int i = 0; i < n; i++)
cin >> p[i];
}
int main() {
int a[10];
read(a, 10);
return 0;
}
#include "Array.h"
#include <iostream>
using namespace std;
void read(int *p, int n) {
for (int i = 0; i < n; i++)
cin >> p[i];
}
int main() {
Array<int> a(10);
read(a, 10);
return 0;
}
调用read函数的时候系统会尝试把对象名转换为int*类型,会编译错误,所以需要定义指针类型转换函数。
//重载指针转换运算符,将Array类的对象名转换为T类型的指针
template <class T>
Array<T>::operator T * () {
return list; //返回当前对象中私有数组的首地址
}
Array类的应用
求范围2~N中的质数,N在程序运行时由键盘输入。
#include <iostream>
#include <iomanip>
#include "Array.h"
using namespace std;
int main() {
Array<int> a(10); // 用来存放质数的数组,初始状态有10个元素。
int n, count = 0;
cout << "Enter a value >= 2 as upper limit for prime numbers: ";
cin >> n;
for (int i = 2; i <= n; i++) {
bool isPrime = true;
for (int j = 0; j < count; j++)
if (i % a[j] == 0) { //若i被a[j]整除,说明i不是质数
isPrime = false; break;
}
if (isPrime) { //如果质数表填满了,将其空间加倍
if (count == a.getSize()) a.resize(count * 2);
a[count++] = i;
}
}
for (int i = 0; i < count; i++) cout << setw(8) << a[i];
cout << endl;
return 0;
}
运行结果:
Enter a value >= 2 as upper limit for prime numbers:100
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
|