共计 28677 个字符,预计需要花费 72 分钟才能阅读完成。
程序设计基础 - 复习
一、算法:求解最大公约数
辗转相除法(欧几里德算法):用较大数除以较小数,然后用除数去除余数,再用出现的余数去除刚才的除数,如此反复,直到余数为 0。这时,除数就是最大公约数。
int gcd(int a, int b) {while (b != 0) {
int temp = b;
b = a % b;
a = temp;
}
return a;
}
更相减损术:用较大数减去较小数,然后用较小数减去差,如此反复,直到两数相等。这时,它们就是最大公约数。
int gcd(int a, int b) {while (a != b) {if (a > b) {a = a - b;} else {b = b - a;}
}
return a;
}
二、保留字和关键字的区别?
保留字:有些语言中预先规定一些单词具有特殊的意义,并保留其名字,不允许程序员另作它用,这种单词被称为保留字。
关键字:还有些语言规定一些特殊单词在一定的上下文中具有预先定义的特殊意义,这种单词被称为关键字。
保留字不一定被用于当前的语法中,所以不一定是关键字。反之,关键字一定是保留字。
保留字:别管我用不用,反正你不能用;
关键字:我用了,你不能用。
三、ASCII 字符代码表
四、头文件(.h 文件)的作用
h 文件:当编写的程序特别大时,为了便于管理和维护,可将完整的程序划分为若干部分,分别存到不同的文件中,通常将类的声明和类的实现分开,类的声明放到 h 文件中,类的实现放到 cpp 中。
五、面向对象程序设计特征
- 数据封装:将一类数据和与该类数据相关的操作集合封装在一起(对象)。
- 继承:低层次的类(子类)可以从它的高层次类(基类)中继承所有的数据和操作。
- 多态性:一个名字,多种语义;相同界面,多种实现。
六、如何不借助变量完成两个数的交换?
a = a + b;
b = a - b;
a = a - b;
七、C++ 中的保留字
- 49 个保留字
- 保留字均为小写
八、关于标识符命名
- 标识符是由字母或下划线开头的字母、数字与下划线的序列。
- 标识符首个字符不能是数字。
- 标识符不可以是保留字。
九、基本数据类型
1 个字节 (byte) 占 8 位(bits)
十、关于进制
- 十进制: 74
- 八进制: 以‘0’前导:0112
- 十六进制: 以‘0x’前导:0x4A
1. 位权表示法
任何一个 N 进制数可表示为:
A=A_n×N_n+A_{n-1}×N_{n-1} … A_1×N_1+A_0×N_0+A_{-1}×N_{-1}+A_{-2} ×N_{-2} … A_{-m}×N_{-m}
例:
(10101.1011)_2 =1×2^4+0×2^3+1×2^2+0×2^1+1×2^0+1×2^{-1}+0×2^{-2}+1×2^{-3} =16+0+4+0+1+0.5+0+0.125=(21.625)_{10}
2. 位权表示法数制的特点
- 数字的总个数等于基数。如十进制使用 10 个数字(0~9)
- 最大的数字比基数小 1。如十进制中最大的数字为 9
- 每个数字都要乘以基数的幂次,该幂次由每个数字所在的位置决定
3. 除基取余法
“除基取余,先余为低(位),后余为高(位)”
例:
(55)_{10}=(110111)_2=(67)_8=(37)_{16}
4. 乘积取整法
”乘基取整,先整为高(位),后整为低(位)”
例:
(0.625)_{10}=(0.101)_2
例:
(0.32)_{10}=(0.0101...)_2
5. 二进制与八进制、十六进制
12 和 012 表示不同数值
八进制:012
十进制:12
十六进制:0x12
+ 二进制数转换为八进制数:以小数点为界,将整数部分自右向左和小数部分自左向右分别按每三位为一组(不足三位用 0 补足),然后将各个三位二进制数转换为对应的一位八进制数。
例:
(10111001010.1011011)_2=(010\quad111\quad001\quad010\quad.\quad101\quad101\quad100)_2=(2712.554)_8
- 八进制数转换为二进制数:把每一位八进制数转换为对应的三位二进制数。
例:
(456.174)_8=(100\quad101\quad110\quad.\quad001\quad111\quad100)_2 =(100101110.0011111)_2 -
二进制数转换为十六进制数:以小数点为界,将整数部分自右向左和小数部分自左向右分别按每四位为一组,不足四位用 0 补足,然后将各个四位二进制数转换为对应的一位十六进制数。
例:
(10111001010.1011011)_2=(0101\quad1100\quad1010\quad.\quad1011\quad0110)_2=(5CA.B6)_{16} -
十六进制数转换为二进制数:把每一位十六进制数转换为对应的四位二进制数。
例:
(1A9F.1BD)_{16}=(0001\quad1010\quad1001\quad1111\quad.\quad0001\quad1011\quad1101)_2=(1101010011111.000110111101)_2
十一、转义字符及其含义
十二、关于浮点数
- 浮点数据的有效位:
单精度浮点型数据的表示以 6 位小数为准,即标准显示 6 位小数(编译系统不同会有所差异),例如 a=1234.567890。但是由于存储空间的限制,单精度浮点数在存储时只存 7 位有效位,即 a 的值存为 1234.567,当输出 a 的值时,显示 1234.567xxx,小数点的后 3 位是随机数。 - 浮点变量:
- float balance; // 4 字节
- double weight=0; // 8 字节
- long double distance; //10 字节
- 浮点数常量:
在默认情况下,浮点常量属于 double 类型,如果希望常量为 float 类型,请使用 f 或 F 后缀。对于 long double 类型,可使用 l 或 L 后缀。
十三、常量定义的两种方式
- 宏定义预处理命令:#define 常量名 常量值
- const 数据类型 标识符名 = 常量值;
十四、关于 C ++ 的运算符以及运算次序
规律:单目运算和带有赋值运算的结合性均为自右往左
十五、自增、自减
前缀形式:++i、--i
后缀形式:i++、i--
它们在表达式中被引用时,前缀表达式是先增(减)1,后被引用;后缀表达式是先被引用,后增(减)1。
十六、关于位运算
&(按位与),|(按位或),^(按位异或),~(取反),<<(左移),>>(右移)
根据有无符号分为算术右移和逻辑右移
+ 算术右移时, 最前面是 1 时, 补 1, 是 0 时, 补 0,以保证符号位的不变
十七、逗号运算
逗号表达式又称顺序求值表达式。
逗号运算符是从左向右结合的:执行逗号表达式时,将从左向右地求值,并以最后一个表达式的值和类型作为该逗号表达式的值和类型。
例:
t=3,t+3 // 逗号表达式的值为 6
十八、运算符之间的优先级
算术运算>关系运算>逻辑运算>赋值运算;
逻辑与 && > 逻辑或 ||;
单目运算符逻辑非!> 其它运算符
例:
a = 3, b = 4, c = 5;
a+b>c&&b==c ===> ((a+b)>c)&&(b==c) ===> 0
a||b+c&&b-c ===> a||((b+c)&&(b-c)) ===> 1
!(a+b)+c-1&&b+c/2 ===> ((!(a+b))+c-1)&&(b+c/2) ===> 1
十九、隐式类型转换
当表达式中存在不同类型的操作数时,为对这个表达式进行求值,编译程序需要对其中的部分操作数自动进行类型转换,将它们都变换为表达式中操作数的“最大”类型, 以保证运算符两边的类型是一致的,这种类型转换称为隐式类型转换。
数据类型的大小:long double > double > float > unsigned long > long > unsigned int > int > unsigned char > char
二十、if 语句
- if 语句的有效范围是单个语句,如果子语句有多个语句,则必须用 {} 括起来,成为一个复合语句。
- C++ 语言中,就近配对:else 总是与它上面的最近的 if 配对。
二十一、条件表达式的短路现象
expression1&&expression2 // expression1 为 0,expression2 不做
expression1||expression2 // expression1 非 0,expression2 不做
当“&&”时,若表达式 1 为 0,则不做表达式 2,直接跳过!!!
当“||”时,若表达式 1 非 0,则不做表达式 2,直接跳过!!!
二十二、关于 case
- case 后只能是常量表达式,不能是变量表达式,其值只能是整型、字符型、枚举型,不能是其他类型。每个常量表达式的值都不相同。
- 若某一个 case 中又定义了一个变量,则必须加{},不然程序无法确定变量的作用域,进而报错。
二十三、关于 break、continue 和 return
二十四、为什么有时采用效率不高的原始算法呢?
- 寻找精美算法,需要对问题本身有很深的理解;效率总是相对而言,人工开销有时候比机器开销成本更大
- 原始算法具有更广泛的适应性,更容易理解和修改
二十五、关于内联函数
由于函数调用代价(开销)较高,需要记录当前指令地址;保护现场;参数传递;执行函数体指令;函数值返回;恢复现场等,因此可以改用内联函数。
内联函数(inline function):对函数的调用被替换为该函数的函数体语句。
内联函数适用于频繁使用、规模较小的函数。
内联函数不是在调用时发生控制转移,而是在编译时将函数体嵌入在每一个调用处。当编译器发现某段代码在调用一个 内联函数 时, 它不是去调用该函数, 而是将该函数的代码, 整段插入到当前位置。这样做的好处是省去了调用的过程。内联扩展是用来消除函数调用时的时间开销。
例:
#include <iostream>
using namespace std;
inline double tax(double gross)
{return gross * 0.05;}
int main()
{
double temp = 5.0;
cout << tax(temp);
temp = temp + 1;
cout << temp;
}
例:
#include <iostream>
using namespace std;
inline int max(int, int, int);
int main()
{
int i = 10, j = 20, k = 30, m;
m = max(i, j, k);
cout << "max=" << m << endl;
}
inline int max(int a, int b, int c)
{
int t = a;
if (b > t) t = b;
if (c > t) t = c;
return t;
}
二十六、同名的全局变量与局部变量
在局部变量的作用域内,全局变量被屏蔽,直接使用该名字,用的是局部变量,但可用“::”作用域运算符引用同名全局变量。
全局变量一般是定义在函数体外面的,程序中所有函数和对象都能引用的变量,默认为外部变量。
二十七、变量的存储类别
变量的定义:数据存储类别 数据类型 变量名(= 初值);
存储类别保留字:auto、register、static、extern
二十八、递归程序
递归调用是无终止的自身调用,因此在递归函数中应该用 if 语句或其它分支语句,判断当某些条件成立时结束递归调用。
例:计算阶乘
#include <iostream>
using namespace std;
int factorial(int n)
{
int result;
if (n == 0) result = 1;
else result = n * factorial(n - 1);
return result;
}
int main()
{cout << "The result of 4! is" << factorial(4) << "\n";
return 0;
}
例:求 Fibonacci(斐波那契)数列
// 使用递归
#include <iostream>
using namespace std;
int fibonacci(int n)
{
int result;
if (n < 2) result = 1;
else result = fibonacci(n - 1) + fibonacci(n - 2);
return result;
}
int main()
{
int loop;// 循环变量
// 输出 Fibonacci 序列的前 6 个数
for (loop = 0; loop <= 6; loop = loop + 1)
{cout << fibonacci(loop) << " ";
}
cout << "\n";
return 0;
}
// 非递归
#include <iostream>
using namespace std;
int fibonacci(int n)
{if (n < 2) return 1;
else
{
int result, i, pre1, pre2 ;
result = 1;
i = 2;
pre2 = 1 ;
while (i <= n)
{
pre1 = pre2 ;
pre2 = result ;
result = pre1 + pre2;
i++;
}
return result;
}
}
int main()
{
int loop;// 循环变量
// 输出 Fibonacci 序列的前 6 个数
for (loop = 0; loop <= 6; loop = loop + 1)
{cout << fibonacci(loop) << " ";
}
cout << "\n";
return 0;
}
二十九、算法:梵塔问题
盘按由小到大的顺序标上号码 1~n。开始时 n 个盘全套在 A 柱上,且小的放在大的上面。游戏要求按下列规则将所有的盘从 A 柱移到 C 柱,在移动过程中可以借助另一个 B 柱。
- 规则 1:每次只能移动柱最上面的一个盘;
- 规则 2:任何盘都不得放在比它小的盘上。
递归思路:
+ 把 A 上的 n 个盘运到 C 上, 借助 B
+ 先把 n - 1 个盘从 A 搬到 B,借助 C
+ 把 A 上剩下的最大的一个盘搬到 C
+ 再把 n - 1 个盘从 B 搬到 C,借助 A
+ 当 n 1 时,直接从 A 搬到 C 即可
#include <iostream>
using namespace std;
// 将 disk_num 个盘从 from 柱移到 to 柱,可以借助 aux 柱
void move_tower(int disk_num, char from, char to, char aux)
{if (disk_num == 1)
{
// 仅有一个盘时,直接从 from 柱移到 to 柱
cout << "Move disk1 from" << from << "to" << to << "\n";
}
else // 将 disk_num - 1 个盘从 from 柱移到 aux 柱,借助于 to 柱
{move_tower(disk_num - 1, from, aux, to);
// 将最下的盘从 from 柱移到 to 柱
cout << "Move disk" << disk_num << "from" << from << "to" << to << "\n";
// 将 disk_num - 1 个盘从 aux 柱移到 to 柱,借助于 from 柱
move_tower(disk_num - 1, aux, to, from);
}
return;
}
int main()
{move_tower(4, 'A', 'C', 'B');
return 0;
}
三十、文件包含 < >和 " "
#include
< 文件名>
编译器将按系统规定的标准方式搜索文件目录。这种格式用于包含 C ++ 系统提供的头文件,这些文件一般存在 C ++ 系统目录的 INCLUDE 子目录下,C++ 预处理程序遇到这条命令后,就到 INCLUDE 子目录下搜索指定的文件,并把它嵌入到当前文件中。-
#include
"文件名"
编译器将首先在当前文件所在的目录下搜索,如果没有找到,则按系统规定的标准方式搜索其它文件目录。这种格式适合于搜索用户自己建立的文件。
三十一、宏定义
1. 符号常量定义
#define
宏名替换正文
宏名习惯上用大写字母
2. 带参数的宏
#define
标识符(参数表)替换正文
“参数表”:用逗号隔开的一个标识符列表。不需要指明数据类型。
宏的运行速度比函数快
注意:带参数的宏只是起到一个文本直接替换的作用,不会默认加上 () 优先运算。
例:
#include <iostream>
using namespace std;
#define PI 3.14159
#define AREA(r) 2.0*PI*r*r
int main()
{
double v = 1.0;
double s1 = AREA(v + 1); // s1=2.0*3.14159*v+1*v+1;
double s2 = AREA((v + 1));
cout << "s1 =" << s1 << endl;
cout << "s2 =" << s2 << endl;
}
/*
输出:s1 = 8.28318
s2 = 25.1327
*/
3. 预处理命令
条件编译:能使同一个源程序在不同的编译条件下产生不同的目标代码文件。它能帮助解决程序的移植问题,也便于对程序进行调试。
一般形式(#else 部分可以缺省):
#ifdef 标识符
程序段 1
#else
程序段 2
#endif
或者
#ifndef 标识符
程序段 1
#else
程序段 2
#endif
条件编译控制命令的更一般形式:
#if 逻辑表达式
程序段 1
#elif
程序段 2
#else
程序段 3
#endif
例:调试程序
#define DEBUG
#undef DEBUG // #undef 命令使得已定义的标识符成为未定义,这样在程序中出现的 #ifdef DEBUG 都是假(0),而程序中所有 #ifndef DEBUG 都是真(非 0)。#ifdef DEBUG
调试用的程序段
#endif
三十二、循环计数器
- void set_mode(int,int); // 设置循环计数器的上、下限
- void set_value(int value); // 设置循环计数器的当前值
- int get_value(); // 查询循环计数器的当前值
- void increment(); // 循环计数器加 1
- void decrement(); // 循环计数器减 1
//CIRNUM3.hpp 声明类,包括类的所有数据成员,以及成员函数的原型
class CIRCULAR_NUMBERS
{
public:
void set_mode(int min, int max); // 设置循环计数器的上、下限
void set_value(int value); // 设置循环计数器的当前值
int get_value(); // 查询循环计数器的当前值
void increment(); // 循环计数器加 1
void decrement(); // 循环计数器减 1
private:
int min_val; // 最小值
int max_val; // 最大值
int current; // 当前值
};
//CIRNUM3.cpp 包含类界面头文件与类实现
#include "cirnum3.hpp"
void CIRCULAR_NUMBERS::set_mode(int min, int max)
{
min_val = min;
max_val = max;
return;
}
void CIRCULAR_NUMBERS::set_value(int value)
{
current = value;
return;
}
int CIRCULAR_NUMBERS::get_value()
{return current;}
// 核心算法————取模
void CIRCULAR_NUMBERS::increment()
{
int mode = max_val - min_val + 1;
current = ((current - min_val) + 1) % mode + min_val;
return;
}
void CIRCULAR_NUMBERS::decrement()
{
int mode = max_val - min_val + 1;
current = ((current - min_val) - 1 + mode) % mode + min_val;
return;
}
三十三、静态数据成员 static
- 用 static 修饰的数据成员:被分配占用一个固定的存储空间,由此类的所有对象共享,对于类来说:静态数据成员相当于类的全局变量。
- 使用方法:
- 在类的界面中(定义)声明一个静态数据成员
- 在类实现中,必须再次声明并初始化这个静态数据变量
三十四、关于构造函数和析构函数的释放时机
1. 单个对象成员的初始化
在 对象生存期 结束时,程序将自动调用该对象所属类的析构函数,调用且仅调用一次。
例:
#include <iostream>
using namespace std;
class DEMO_CLASS
{
public:
DEMO_CLASS(int i);
~DEMO_CLASS();};
DEMO_CLASS::DEMO_CLASS(int i)
{
cout << "Initial value is" << i << "\n";
return;
}
DEMO_CLASS::~DEMO_CLASS()
{
cout << "Goodbye!\n";
return;
}
int main()
{DEMO_CLASS obj(30);// 声明一个对象
cout << "This is the end of main().\n";
return 0;
}
/*
输出:Initial value is 30
This is the end of main().
Goodbye!
*/
这里可以看到,析构函数调用是在程序结尾,输出“This is the end of main().”之后才被调用的。正如上面所说的,在 对象生存期 结束时,程序将自动调用该对象所属类的析构函数,而本程序对象的生存期是定义后的整个主函数,因此在主函数执行完毕后才调用析构函数。
2. 多个对象成员的初始化
如果有多个对象成员,对象成员构造函数的调用次序取决于这些成员在类中声明的次序。
执行次序:创建类的对象时,调用此类的构造函数,先根据初始化列表调用对象成员的构造函数,初始化对象成员,然后才执行本身的构造函数的函数体。析构函数调用的次序相反。
这里说析构函数调用的次序相反,其实很好理解,遵循先入后出的原则,与栈相似,相当于在瓶子中装东西,最后装的肯定会被先拿出来。
例:
#include <iostream>
using namespace std;
class Object
{
int val;
public:
Object( ); // 构造函数
Object(int i); // 构造函数
~Object(); // 析构函数};
Object::Object( )
{
val = 0;
cout << "\n Default constructor for Object.\n";
}
Object::Object(int i) // 构造函数定义
{
val = i;
cout << "\n Constructor for Object:" << val << endl;
}
Object::~Object( ) // 析构函数定义
{cout << "\n Destructor for Object:" << val << endl;}
class Container // 定义新类,它含 Object 的对象
{
private:
int date;
Object one; // 对象成员
Object two; // 对象成员
public:
Container( ); // 构造函数
Container(int i, int j, int k); // 构造函数
~Container(); // 析构函数};
Container::Container( ) // 构造函数定义
{
date = 0;
cout << "\n Default constructor for Container.\n";
}
Container::Container(int i, int j, int k): two(i), one(j)
{
date = k;
cout << "\n Constructor for Container.\n";
}
Container::~Container( )
{cout << "\n Destructor for Container.\n";}
int main( ) // 演示程序
{
cout << "\n... Entering main ...\n";
Container obj(5, 6, 10);
cout << "\n... Exiting main ...\n";
}
/*
输出:... Entering main ...
Constructor for Object: 6
Constructor for Object: 5
Constructor for Container.
... Exiting main ...
Destructor for Container.
Destructor for Object: 5
Destructor for Object: 6
*/
分析:首先调用 Container 的构造函数,但是先执行初始化列表操作,调用 Object 的构造函数,因此最先显示的是两行 Object 的构造函数;在初始化列表执行完毕之后,继续执行刚刚的 Container 构造函数的函数体,显示“Constructor for Container.”。然后由于对象的生存期是定义后的整个主函数,所以在初始化对象之后没有立即析构,先显示”... Exiting main ...“。在主函数执行完毕时,对象被销毁,调用析构函数。根据先入的后出的原则,因此只需按构造的顺序倒回去即可。
3. 常量数据成员的初始化
常量数据成员不能在类的声明中初始化(类中不能用初值表达式),也不能在构造函数中设置其值(赋值操作不能用于常量),只能采用初始化列表中来赋值。
三十五、动态创建于撤销
1. 创建
运算符 new 提供了动态存储分配的功能。
一般形式:指针名 = new 基类型名(初始化表);
若基类型是类类型,则 () 内为该类的构造函数提供实参。
功能:动态分配一块内存,其大小由基类型决定,若分配成功,则返回该内存的首地址给指针,否则返回 NULL。
2. 撤销
运算符 delete 用于释放用 new 分配的存储空间。
用 new 分配的内存空间,一定要用 delete 释放,而且只能释放一次。否则将产生“内存垃圾”。
一般形式:delete 指针名;
数组释放时,加 [] 则会释放整个分配的区域,否则只会释放一个基类型占用的空间;
三十六、对象在拷贝后出现的释放问题
例:
class Student
{
public:
Student(const char *str)
{name = new char[strlen(str) + 1];
strcpy(name, str);
…
}
~Student()
{
delete name;
…
}
private:
char *name; // name 以指针形式出现
}
Student s1("Li4"), s2("zhang3");
…
s1 = s2; // s2 中 name 指针的值复制给 s1 中的 name
在执行 "s1 = s2" 时,就会产生问题。
在 C ++ 中默认重载了“=”,使得两个对象之间可以通过“=”来进行拷贝,即将“=”右侧的数据成员逐位复制给“=”左侧的数据成员。
在本程序中,首先分别创建对象 s1 和 s2,并分别堆区开辟内存,存储字符串“Li4”和“Zhang3”,然后执行“s1 = s2;”。在执行“=”后,s1 的 name 指针将会指向 s2 的 name 的地址,即“Zhang3”,而原来的“Li4”则没有指针指向它。在调用析构函数时,也就无法清理这块内存,导致堆区产生内存垃圾。
解决办法:设计一个专门的复制函数 Student *copy(Student *obj1, Student *obj2);
三十七、关于指针的运算
- 自增自减
例:int age = 30; int* age_ptr; age_ptr = &age; *age_ptr = 50;
自增运算 ++ 优先级等于指针运算 *,但是结合性是自右向左方向
- age++; 等价于 (*age_ptr)++;
- *age_ptr++; 等价于 *(age_ptr++);
-
加减
p±n 后得到的地址:p 原来指向的地址±n例:
int a = 10; // 设 a 的地址是 2000H int *p1 = &a ; // p1 存放的地址是 2000H p1++; // int 两个字节,地址变为 2002H
三十八、函数作为左值
函数可以返回一个引用,将函数说明为返回引用类型后,函数可以出现在赋值运算符的左边,即作为左值。
例:
#include <iostream>
using namespace std;
int a[] = {1, 2, 3, 4, 5};
int & index(int); // 说明返回引用的函数
int main()
{for (int i = 1; i < 5; i++)
index(0) += index(i); // 求数组各元素之和并放在数组第 0 个元素中
cout << "sum=" << index(0) << endl; // 函数作为左值
}
int & index(int i)
{return a[i];
}
三十九、算法:冒泡排序
#include <iostream>
using namespace std;
// 核心算法
void bubbleSort(int arr[], int n)
{for (int i = 0; i < n - 1; i++)
{for (int j = 0; j < n - i - 1; j++)
{if (arr[j] > arr[j + 1])
{// 交换 arr[j]和 arr[j+1]
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
int main()
{
const int max_nums = 8; // 程序要处理数据的个数作为符号常量
int data[max_nums] = {11, 25, 8, 62, 53, 68, 99, 23}; // 存放数据的数组
cout << "Before sorting:";
for (int i = 0; i <= max_nums - 1; i++)
cout << data[i] << " ";
cout << endl;
// 对数据排序
bubbleSort(data, max_nums);//*******//
// 将排序后的数据输出
cout << "After sorting:";
for (int i = 0; i <= max_nums - 1; i++)
cout << data[i] << " ";
return 0;
}
四十、算法:交换子数列段
给定含有 m + n 个元素的整型数组 A(其中 m >0, n>0),它分为两个互不重叠的,长度分别为 m 和 n 的子数组段,写出交换这两个子数组段的程序,要求不引入数组 A 以外的数组,但可引入若干中间变量。
#include <iostream>
using namespace std;
const int MAX = 100;
int main()
{int a[MAX], m, n, i, j, t;
cin >> m >> n;
for (i = 0; i < m + n; i++)
cin >> a[i];
// 核心算法
for (i = m - 1; i >= 0; i--)
{t = a[i];
for (j = 0; j < n; j++)
a[i + j] = a[i + j + 1];
a[i + n] = t;
}
for (i = 0; i < m + n; i++)
cout << a[i] << ' ';
cout << endl;
}
四十一、算法:折半查找法
数组 a 中的 n 个数从大到小(降序)的顺序排列,要检索一个数 x 是否在 a 中,折半查找算法的思路是:
设查找区间为[lower,upper],初值 lower=0, upper=n-1; 算法步骤:
- 输入数组 a 的元素及 x;
- x 是否为 a 的第一或最后一个元素。若是其中之一,算法结束;
- 将区间两等分为 [lower, mid] 和[mid, upper],mid 为区间中点,每次取出中间项进行检查,若 xa[mid],x 被检索到,算法结束;若 x<a[mid],则在后半区间 [mid, upper] 内重复折半检索;若 x>a[mid],则在前半区间 [lower, mid] 内重复折半检索。反复执行上述步骤,使 lower 和 upper 相等或相差 1(表示未检索到 x)。
迭代实现:
int binarySearch(int array[], int x, int low, int high) {while (low <= high) {int mid = low + (high - low) / 2;
if (array[mid] == x)
return mid;
if (array[mid] < x)
low = mid + 1;
else
high = mid - 1;
}
return -1;
}
递归实现:
int binarySearch(int array[], int x, int low, int high) {if (low > high)
return -1;
else {int mid = (low + high) / 2;
if (x == array[mid])
return mid;
else if (x > array[mid])
return binarySearch(array, x, mid + 1, high);
else
return binarySearch(array, x, low, mid - 1);
}
}
四十二、指针数组与数组指针
[]的优先级大于 *;如果需要保留许多指针,可以使用指针数组;数组指针 b 指向一个长度为 5 的整形数组。
- 指针数组:基类型为指针的数组——int a[5];
- 数组指针:基类型为数组的指针——int (*b)[5];、int array[5];、b = array;
四十三、字符数组的长度问题
char str[] = {“I am happy”}; // 字符串长度 10,数组长度 11
char str[] =“I am happy”; // 字符串长度 10,数组长度 11
char str1[] = {‘I’,‘’,‘a’,‘m’,‘’,‘h’,‘a’,‘p’,‘p’,‘y’}; // 数组长度 10
四十四、算法:字符串的复制
#include <iostream>
using namespace std;
// 实现 1
void copy_string1(char from[], char to[])
{
int k = 0;
while (from[k] != '\0')
{to[k] = from[k];
k++;
}
to[k] = '\0';
}
// 实现 2
void copy_string2(char *from, char *to)
{
int k = 0;
while ((to[k] = from[k]) != '\0') k++;
}
// 实现 3
void copy_string3(char *from, char *to)
{for (; *from != '\0'; from++, to++)
*to = *from;
*to = '\0';
}
// 实现 4
void copy_string4(char *from, char *to)
{while ((*to = *from) != '\0')
{
from++;
to++;
}
}
// 实现 5
void copy_string5(char *from, char *to)
{while (( *to++ = *from++) != '\0');
}
int main()
{char a[] = "I am a teacher";
char b[20];
copy_string1(a, b);
cout << b << endl;
copy_string2(a, b);
cout << b << endl;
copy_string3(a, b);
cout << b << endl;
copy_string4(a, b);
cout << b << endl;
copy_string5(a, b);
cout << b << endl;
return 1;
}
四十五、算法:单词排序
对输入的英文单词按字典顺序由小到大排序后输出。
#include <iostream>
#include <string.h>
using namespace std;
const int N = 10;
int readword(char *word[])
{char str[20];// 临时数组保存一个单词
int i = 0, l;
cin >> str;
while (strcmp(str, "end"))
{l = strlen(str);
word[i] = new char[l + 1];
strcpy(word[i], str);
i++;
cin >> str;
}
return i;
} // 用指针数组保存多个英文单词
void writeword(char *word[], int n)
{while (--n >= 0)
cout << *word++ << endl;
}
void sort(char *w[], int n)
{
int i, j;
char *t;
for (i = 0; i <= n - 2; i++)
for (j = n - 2; j >= i; j--)
if (strcmp(w[j], w[j + 1]) > 0)
{t = w[j];
w[j] = w[j + 1];
w[j + 1] = t;
}
}
int main()
{char *word[N];
int num;
num = readword(word);
sort(word, num);
cout << endl;
writeword(word, num);
return 1;
}
四十六、枚举类型
如果一个变量只有几种可能的值,则可声明一个枚举类型。
enum 枚举类型名{枚举元素 1, 枚举元素 2, ……};
例:
enum WEEKDAY{sun, mon, tue, wed, thu, fri, sat};
WEEKDAY day;
day = mon;
- 枚举元素是 (不重复的) 整型常量,缺省时,按顺序为每个元素指定常量值。sun 为 0,mon 为 1。
-
可显式为枚举元素指定常量值
例:
enum META_COLOR {// 基本颜色类型(RGB)RED = 4, // 红色 GREEN = 2, // 绿色 BLUE = 1 // 蓝色 };
- 若类型声明时为某个元素指定常量值,那么其后没有显式赋值的元素,其值为上一个常量值加 1。
例:
enum COLOR { // 颜色类型 RED, // 红色 RED 的值为 0 GREEN = 5, // 绿色 GREEN 的值为 5 BLUE = 6, // 蓝色 BLUE 的值为 6 CYAN // 青色 CYAN 的值为 7 };
枚举值可做判断:if (day>tue) ……
不能把整数直接赋给枚举变量:
+ day = 2 ; 错
+ day = (WEEKDAY) 2 ; 对
四十七、筛选法求素数
筛选法求出 2~N 间的所有素数的方法是:首先将这些数全部放入一个数组中,然后重复下面的操作直到数组为空为止:
- 找出其中的最小数 K,则 K 一定是一个素数,因此可输出。
- 从数组中删除 K 及其所有倍数。
法 1:
#include <iostream>
#include <vector>
using namespace std;
void sieveOfEratosthenes(int n)
{vector<bool> prime(n + 1, true);
for (int p = 2; p * p <= n; p++)
{if (prime[p] == true)
{for (int i = p * p; i <= n; i += p)
prime[i] = false;
}
}
for (int p = 2; p <= n; p++)
{if (prime[p])
cout << p << " ";
}
}
int main()
{
int n = 100;
cout << "Prime numbers smaller than or equal to" << n << "are:";
sieveOfEratosthenes(n);
return 0;
}
法 2:
#include <iostream>
using namespace std;
int find_prime(int* X, int n)
{
int temp, i;
if (X[0] == 0)
return 0;
else
{temp = X[0];
for (i = 0; i < n - 1; i++)
X[i] = X[i + 1];
for (i = 0; i < n - 1; i++)
{if (X[i] > 0 && X[i] % temp == 0)
{for (int k = i; k < n - 1; k++)
X[k] = X[k + 1];
i--;
}
}
return temp;
}
}
int main()
{
int N, i;
int *X, *Y;
cout << "please input N=";
cin >> N;
X = new int[N];
for (i = 0; i < N - 1; i++)
X[i] = i + 2;
X[N - 1] = 0;
Y = new int[N - 1];
for (i = 0; i < N - 1; i++)
Y[i] = 0;
for (i = 0; i < N - 1; i++)
{Y[i] = find_prime(X, N);
if (Y[i] == 0)
break;
}
for (i = 0; i < N - 1; i++)
{if (Y[i] > 0)
cout << Y[i] << "\n";
}
return 0;
}
四十八、成员和继承访问控制规则
四十九、继承的类型兼容性
- 类型的赋值兼容性规则允许将后代类的对象赋值给祖先类,但反之不成立。但此规则只适用于公有派生,只有公有派生类才能兼容基类类型。
例:
-
指向基类对象的指针也可指向公有派生类对象。
例:
五十、继承中构造函数与析构函数的调用次序
1. 单继承
1. 构造函数的调用次序
在创建一个派生类的对象时(“尊老爱幼”)
+ 先调用其基类的构造函数
+ 再调用本类对象成员的构造函数
+ 最后才调用本类的构造函数
2. 析构函数的调用次序
- 先调用本类的析构函数
- 再调用本类对象成员的析构函数
- 最后才调用其基类的析构函数
例:
#include <iostream>
using namespace std;
class C
{
public:
C() // 构造函数
{cout << "Constructing C object.\n";}
~C() // 析构函数
{cout << "Destructing C object.\n";}
};
class BASE
{
public:
BASE() // 构造函数
{cout << "Constructing base object.\n";}
~BASE() // 析构函数
{cout << "Destructing base object.\n";}
};
class DERIVED: public BASE
{
C mOBJ;
public:
DERIVED() // 构造函数
{cout << "Constructing derived object.\n";}
~DERIVED() // 析构函数
{cout << "Destructing derived object.\n";}
};
int main()
{
DERIVED obj; // 声明一个派生类的对象
// 什么也不做,仅完成对象 obj 的构造与析构
return 0;
}
/*
输出:Constructing base object.
Constructing C object.
Constructing derived object.
Destructing derived object.
Destructing C object.
Destructing base object.
*/
2. 多重继承
多个基类构造函数的调用次序是按基类在被继承时所声明的次序、从左到右依次调用的,与它们在派生类构造函数实现中的初始化列表中出现的次序无关。
例:
#include <iostream>
using namespace std;
class Base1
{
public:
Base1(int x)
{cout << x << "->Constructing base1 object.\n";}
~Base1()
{cout << "Destructing base1 object.\n";}
};
class Base2
{
public:
Base2(int x)
{cout << x << "->Constructing base2 object.\n";}
~Base2()
{cout << "Destructing base2 object.\n";}
};
class Derived: public Base2, public Base1
{
public:
Derived(int x, int y): Base1(x), Base2(y)
{cout << "Constructing derived object.\n";}
~Derived()
{cout << "Destructing derived object.\n";}
};
int main()
{Derived obj(10, 20);
return 0;
}
/*
输出:20->Constructing base2 object.
10->Constructing base1 object.
Constructing derived object.
Destructing derived object.
Destructing base1 object.
Destructing base2 object.
*/
3. 虚基类
- 对虚基类构造函数的调用总是先于普通基类的构造函数。
- 虚基类的唯一副本只被初始化一次。
- C++ 中构造函数的调用次序:
- 最先调用虚基类的构造函数。
- 其次调用普通基类构造函数,多个基类则按派生类声明时列出的次序、从左到右调用,而不是初始化列表中的次序。
- 再次调用对象成员的构造函数,按类声明中对象成员出现的次序调用,而不是初始化列表中的次序。
- 最后执行派生类的构造函数。
例:
#include <iostream>
using namespace std;
class BaseA
{
public:
BaseA()
{cout << "\nThis is BaseA Constructing. \n";}
~BaseA()
{cout << "\nThis is BaseA Destructing. \n";}
};
class BaseB
{
public:
BaseB()
{cout << "\nThis is BaseB Constructing. \n";}
~BaseB()
{cout << "\nThis is BaseB Destructing. \n";}
};
class DerivedA: public BaseB, virtual public BaseA
{
public:
DerivedA()
{cout << "\nThis is DerivedA Constructing. \n";}
~DerivedA()
{cout << "\nThis is DerivedA Destructing. \n";}
};
class DerivedB: public BaseB, virtual public BaseA
{
public:
DerivedB()
{cout << "\nThis is DerivedB Constructing. \n";}
~DerivedB()
{cout << "\nThis is DerivedB Destructing. \n";}
};
class Derived: public DerivedA, virtual public DerivedB
{
public:
Derived()
{cout << "\nThis is Derived Constructing. \n";}
~Derived()
{cout << "\nThis is Derived Destructing. \n";}
};
int main()
{
Derived obj;
return 0;
}
/*
输出:This is BaseA Constructing.
This is BaseB Constructing.
This is DerivedB Constructing.
This is BaseB Constructing.
This is DerivedA Constructing.
This is Derived Constructing.
This is Derived Destructing.
This is DerivedA Destructing.
This is BaseB Destructing.
This is DerivedB Destructing.
This is BaseB Destructing.
This is BaseA Destructing.
*/
由于 baseA 为 derivedA 和 derivedB 的虚基类,所以派生类 derivedA 和的 derivedB 共用 baseA 的一个实例,这个实例已经在 DerivedB 中被初始化了。
五十一、重复继承的二义性
若在继承时没有作特殊声明,此时采用的是复制继承,会导致重复继承的二义性问题。
例:
class Base
{
public:
int i;
};
class Base1: public Base
{
public:
int j;
};
class Base2:
public Base
{
public:
int k;
};
class DERIVED: public Base1, public Base2
{
public:
int sum;
};
int main()
{
DERIVED obj; // 声明一个派生类对象
obj.i = 3; // 错误,编译程序无法确定使用 i 的哪一份副本
obj.j = 5; // 正确的,使用从 Base1 继承下来的 j
obj.k = 7; // 正确的,使用从 Base2 继承下来的 k
}
解决办法:
1. 使用作用域运算符,必须维持两个副本的一致性,显得不自然。
2. 改用共享方式继承:用虚基类机制保证任何派生类中只提供一个基类的副本。
如果基类被申明为虚基类, 那么重复继承的基类在派生类对象实例中只存储一个副本。
五十二、重载和重定义的区别
- 函数重载:派生类中增加了一个与基类成员函数同名的成员函数,但函数的形式参数个数、类型或次序不同,编译程序认为是增加了一个新的成员函数。
- 重定义:派生类中定义了一个函数原型与继承成员函数一样的成员函数,但语义上修改了成员函数的实现。
对成员函数重定义后,派生类对象使用函数名进行函数调用,那么使用的是派生类中新定义的成员函数。此时称这个派生类中的成员的名字支配了基类的名字。
继承成员被重定义后,若想继续使用基类被屏蔽了的成员,可用:基类名:: 成员名 来使用。
五十三、函数重载时的二义性
1. 隐式类型转换引起
#include <iostream>
using namespace std;
float abs(float x) // 浮点类型数据的绝对值函数
{return (x >= 0 ? x : -x);
}
double abs(double x) // 双精度浮点类型数据的绝对值函数
{return (x >= 0.0 ? x : -x);
}
int main()
{cout << abs(3.14) << endl; // 调用 abs()的 double 版本
cout << abs(-5) << endl; // 错误!无法确定调用 abs()的哪个版本?return 0;
}
默认为双精度。
2. 使用缺省参数引起
#include <iostream>
using namespace std;
int func(int i)
{return i;}
int func(int i, int j = 10)
{return i * j;}
int main()
{cout << func(3, 4) << "\n"; // 调用 func(int, int)
cout << func(5) << "\n"; // 错误,编译程序无法确定调用 func()的哪一个版本
return 0;
}
五十四、运算符重载
1. 类成员运算符重载
一般形式:
类型 类名::operator @(参数表)
{… // 运算符函数体}
不可由派生类继承下去
2. 友元运算符重载
例:
#include <iostream>
using namespace std;
class INTEGER
{
public:
INTEGER(int i = 0)
{
value = i;
return;
}
INTEGER(const INTEGER &other)
{
value = other.value;
return;
}
friend INTEGER operator + (INTEGER left, INTEGER right)
{
INTEGER temp;
temp.value = left.value + right.value;
return temp;
}
void print_int()
{cout << value << endl;}
private:
int value;
};
int main()
{INTEGER x(10);
INTEGER y = x;
INTEGER z;
x.print_int();
y.print_int();
y = x + 2;
z = 30 + y;
y.print_int();
z.print_int();
return 0;
}
/*
输出:10
10
12
42
*/
可由派生类继承下去
3. 重载运算符“[]”
#include <iostream>
using namespace std;
class Array
{
private:
int arr[5] = {1, 2, 3, 4, 5};
public:
int& operator[](int index)
{return arr[index];
}
};
int main()
{
Array myArray;
cout << myArray[2] << endl; // 这里将调用重载的 [] 运算符
myArray[2] = 10; // 这里也将调用重载的 [] 运算符
cout << myArray[2] << endl;
return 0;
}
/*
输出:3
10
*/
4. 重载运算符“<<”和“>>”
- 重载 << 运算符:
#include <iostream> using namespace std; class MyClass { public: int data; friend ostream& operator<<(ostream& os, const MyClass& obj) { os << "Data:" << obj.data; return os; } }; int main() { MyClass obj; obj.data = 10; cout << obj; // 这里将调用重载的 << 运算符 return 0; }
- 重载>>运算符:
#include <iostream> using namespace std; class MyClass { public: int data; friend istream& operator>>(istream& is, MyClass& obj) { cout << "Enter data:"; is >> obj.data; return is; } }; int main() { MyClass obj; cin >> obj; // 这里将调用重载的 >> 运算符 cout << "Data entered:" << obj.data; return 0; }
五十五、拷贝构造函数
1. 调用时机
例:
#include <iostream>
using namespace std;
class TEST
{
public:
TEST(int i)
{
cout << "Constructinbg with" << i << endl;
value = i;
return;
}
~TEST()
{
cout << "Destructing." << endl;
return;
}
int get()
{return value;}
private:
int value;
};
void display(TEST obj) // 对象作为函数的参数,按值调用方式传递
{cout << "Display with" << obj.get() << ".\n";
return;
}
int main()
{TEST explorer(5);
display(explorer);
return 0;
}
/*
输出:Constructinbg with 5
Display with 5.
Destructing.
Destructing.
*/
当函数按值调用传递对象时,传递的是对象的一个副本,而不是对象本身,因而,函数体内对对象参数的修改并不会改变对象的值;与此同时,创建副本对象时,并没有调用构造函数,因为调用构造函数需要初始化对象的状态;相反,调用的是拷贝构造函数。
2. 重写拷贝构造函数
- 形式:
CLASS_NAME::CLASS_NAME(const CLASS_NAME &object) {...... // 函数体}
- 该参数传递方式为按引用调用:使形参为实参的别名,避免在函数调用过程中生成形参副本。
- 该形参一般声明为 const:因为按引用调用是双向的,为确保在拷贝构造函数中不修改实参的值,所以用 const。
3. 对象作为函数返回值产生的问题
#include <iostream>
#include <string.h>
using namespace std;
class Name
{
public:
Name()
{
cout << "Constructing" << endl;
string = NULL;
return;
}
~Name()
{
cout << "Destructing." << endl;
if (string != NULL)
delete [] string;
return;
}
void show()
{
cout << string << endl;
return;
}
void set(char *s)
{if (string != NULL)
delete [] string;
string = new char[strlen(s) + 1];
strcpy(string, s);
return;
}
private:
char *string;
};
Name get_name()
{
Name temp_obj;
char temp_str[20];
cout << "Input your name:";
cin >> temp_str;
temp_obj.set(temp_str);
return temp_obj;
}
int main()
{
Name myname;
myname = get_name();
myname.show();
return 0;
}
当函数返回一个对象作为返回值时, 编译程序将自动创建一个临时对象,临时对象调用拷贝构造函数进行初始化;temp_obj 对象的 string 指针和临时对象的 string 指针同时指向字符串,但是释放了两次,产生内存问题;
当重写拷贝构造函数后:
#include <iostream>
#include <string.h>
using namespace std;
class Name
{
public:
Name()
{
cout << "Constructing" << endl;
string = NULL;
}
Name(const Name &other)
{if (other.string != NULL)
{string = new char[strlen(other.string) + 1];
strcpy(string, other.string);
}
cout << "Copy constructing.\n";
}
~Name()
{
cout << "Destructing." << endl;
if (string != NULL)
delete [] string;}
void show()
{
cout << string << endl;
return;
}
void set(char *s)
{if (string != NULL)
delete [] string;
string = new char[strlen(s) + 1];
strcpy(string, s);
return;
}
private:
char *string;
};
Name get_name()
{
Name temp_obj;
char temp_str[20];
cout << "Input your name:";
cin >> temp_str;
temp_obj.set(temp_str);
return temp_obj;
}
int main()
{
Name myname;
myname = get_name();
myname.show();
return 0;
}
当函数返回一个对象作为返回值时,编译程序将自动创建一个临时对象,临时对象调用拷贝构造函数进行初始化;重写拷贝构造函数为字符串新分配了内存,但还是没有解决内存问题,临时对象的 string 指针和 myname 对象的 string 指针同时指向字符串,但是释放了两次,依旧产生内存问题。
解决方法,重载运算符
#include <iostream>
#include <string.h>
using namespace std;
class Name
{
public:
Name()
{
cout << "Constructing" << endl;
string = NULL;
}
Name(const Name &other)
{if (other.string != NULL)
{string = new char[strlen(other.string) + 1];
strcpy(string, other.string);
}
cout << "Copy constructing.\n";
}
~Name()
{
cout << "Destructing." << endl;
if (string != NULL)
delete [] string;}
Name operator = (const Name &other)
{if (other.string != NULL)
{string = new char[strlen(other.string) + 1];
strcpy(string, other.string);
}
cout << "operator =.\n";
return *this;
}
void show()
{
cout << string << endl;
return;
}
void set(char *s)
{if (string != NULL)
delete [] string;
string = new char[strlen(s) + 1];
strcpy(string, s);
return;
}
private:
char *string;
};
Name get_name()
{
Name temp_obj;
char temp_str[20];
cout << "Input your name:";
cin >> temp_str;
temp_obj.set(temp_str);
return temp_obj;
}
int main()
{
Name myname;
myname = get_name();
myname.show();
return 0;
}
/*
输入:wang
输出:Constructing
Constructing
Input your name: wang
operator =.
Copy constructing.
Destructing.
Destructing.
wang
Destructing.
*/
五十六、指向基类对象的指针指向该基类的的公有派生类对象
指向基类对象的指针可以指向该基类的的公有派生类对象,这是运行时多态性的基础。指向基类对象的指针指向派生类对象后,可以访问派生类对象中由基类继承下来的成员,但不能访问那些派生类对象自己增加的成员。
通过指向基类的指针实现动态绑定,直接使用对象本身无法实现动态绑定。
普通成员函数的调用采用的是静态绑定,通过指针引起的普通成员函数调用,仅仅与指针(或引用)的基类型有关,而与此刻该指针正在指向什么对象无关。
指向基类的指针不能访问那些派生类对象自己增加的成员。
例:
#include <iostream>
using namespace std;
class BASE
{
public:
void who()
{cout << "BASE\n";}
};
class FIRST_D: public BASE
{
public:
void who() // 继承成员的重定义
{cout << "First Derivation\n";}
};
class SECOND_D: public BASE
{
public:
void who()
{cout << "Second Derivation\n";}
};
int main()
{
BASE b_obj;
FIRST_D f_obj;
SECOND_D s_obj;
BASE *p; // 定义指向基类的指针 p
p = &b_obj;
p->who();
p = &f_obj;
p->who(); // 根据继承的赋值兼容性
p = &s_obj;
p->who(); // 基类指针可指向派生类对象}
/*
输出:BASE
BASE
BASE
*/
五十七、虚函数
虚函数在公有继承结构中在一个或多个派生类中被重定义,在被调用过程中,通过指向基类的指针实现动态绑定(直接使用对象本身无法实现动态绑定)。
基类的虚函数定义了一种接口,在派生类中为此接口定义不同的实现版本,由于虚函数的解释机制,实现“单界面、多实现版本”的思想,在运行时刻才将函数与不同实现版本进行匹配。——运行时的动态绑定、多态性
例:
#include <iostream>
using namespace std;
class BASE
{
public:
virtual void who() // 希望 p ->who(); 依赖于运行时 p 所指向的对象
{cout << "BASE\n";}
};
class FIRST_D: public BASE
{
public:
void who() // 继承成员的重定义
{cout << "First Derivation\n";}
};
class SECOND_D: public BASE
{
public:
void who()
{cout << "Second Derivation\n";}
};
int main( )
{
BASE b_obj;
FIRST_D f_obj;
SECOND_D s_obj;
BASE *p; // 定义指向基类的指针 p
p = &b_obj;
p->who();
p = &f_obj;
p->who(); // 根据继承的赋值兼容性
p = &s_obj;
p->who(); // 基类指针可指向派生类对象}
/*
输出:BASE
First Derivation
Second Derivation
*/
例:
#include <iostream>
using namespace std;
class BASE
{
public:
virtual void f1( )
{......}
virtual void f2( )
{......}
virtual void f3( )
{......}
void f ( )
{......}
};
class DERIVED: public BASE
{
public:
void f1( )
{......} // 虚函数的重定义,f1 在该类中还是虚的
void f2(int a)
{......} // 函数重载,虚特性丢失,参数表改变
// char f3() { ……} // 编译出错,返回类型改变
void f ( )
{......} // 普通函数的重定义
};
void main( )
{
DERIVED d;
BASE *p = &d; // 基类指针 p 指向派生类对象
p->f1(); // 调用 DERIVED::f1(); 动态绑定
p->f2(); // 调用 BASE::f2(); 静态绑定
p->f (); // 调用 BASE::f(); 静态绑定
((DERIVED *)p)->f2(100); // 强制类型转换,调用派生类重载后的函数 DERIVED::f2(); 静态绑定}
五十八、纯虚函数与抽象类
纯虚函数是一个在基类中声明的虚函数,但在基类中没有定义函数体,要求任何派生类都必须定义自己的版本。
一般形式:virtual 函数类型 函数名(形参表)=0;
如果一个类中至少有一个纯虚函数,则该类为抽象类
抽象类的特性:
+ 只能用作其他类的基类
+ 不能用于直接创建对象实例:FIGURE figure // 出错
+ 不能做函数的形参类型、返回类型或显式类型转换。
+ 可声明抽象类的指针和引用
例:
五十九、关于 iostream
streambuf 是负责流缓冲区的类,has- a 关系。
- 输入:从流中提取一个字符序列,“>>”提取符 在类 istream 中定义。
- 输出:向流中插入一个字符序列,“<<”插入符 在类 ostream 中定义。
四个预定义的流对象:
- cin: 标准输入,缺省为键盘
- cout: 标准输出,缺省为显示器
- cerr: 无缓冲的标准错误输出,缺省为显示器
- clog: 有缓冲的标准错误输出,缺省为显示器
六十、关于 fstream
文件类型分为两种:
+ 文本文件:文件以文本的 ASCII 码形式存储在计算机中
+ 二进制文件 - 文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们
操作文件的三大类:
+ ofstream:写操作
+ ifstream:读操作
+ fstream:读写操作
1. 写文件
写文件步骤:
- 包含头文件
#include <fstream> -
创建流对象
ofstream ofs; -
打开文件
ofs.open("文件路径", 打开方式); -
写数据
ofs << "写入的数据"; -
关闭文件
ofs.close();
文件打开方式:
打开方式 | 解释 |
---|---|
ios::in | 为读文件而打开文件 |
ios::out | 为写文件而打开文件 |
ios::ate | 初始位置:文件尾 |
ios::app | 追加方式写文件 |
ios::trunc | 如果文件存在先删除,再创建 |
ios::binary | 二进制方式 |
注意:
* 文件操作必须包含头文件 fstream
* 读文件可以利用 ofstream,或者 fstream 类
* 打开文件时候需要指定操作文件的路径,以及打开方式
* 利用 << 可以向文件中写数据
* 操作完毕,要关闭文件
2. 读文件
读文件步骤如下:
- 包含头文件
#include <fstream> -
创建流对象
ifstream ifs; -
打开文件并判断文件是否打开成功
ifs.open("文件路径", 打开方式); -
读数据
-
关闭文件
ifs.close();
例:
#include <fstream>
#include <string>
void test01()
{
ifstream ifs;
ifs.open("test.txt", ios::in);
if (!ifs.is_open())
{
cout << "文件打开失败" << endl;
return;
}
// 第一种方式
//char buf[1024] = {0};
//while (ifs >> buf)
//{
// cout << buf << endl;
//}
// 第二种
//char buf[1024] = {0};
//while (ifs.getline(buf,sizeof(buf)))
//{
// cout << buf << endl;
//}
// 第三种
//string buf;
//while (getline(ifs, buf))
//{
// cout << buf << endl;
//}
char c;
while ((c = ifs.get()) != EOF)
{cout << c;}
ifs.close();}
int main() {test01();
system("pause");
return 0;
}
注意:
- 读文件可以利用 ifstream,或者 fstream 类
- 利用 is_open 函数可以判断文件是否打开成功
- close 关闭文件
六十一、getline 函数
inline istream& getline(char *,int,char ='\n');
// 从指定输入流中读 int 个字符,函数在读到比 int 少一个字符或遇到字符 char 为止(char 缺省为’\n’),并补上字符串结束符’\0’。与带 3 个参数 get 成员函数不同的是 geline 要从输入流中删除分隔符(即读取并删除它),而并不是把它存放在 buffer 中。inline istream& getline(unsigned char *,int,char ='\n');
inline istream& getline(signed char *,int,char ='\n');
六十二、get 函数
int get(); // 从输入流中读入一个字符,返回该字符作为函数调用值,遇到文件结束符,返回 EOF。(EOF:文件结束符,为符号常量,值为 -1)
istream& get(char &); // 从指定输入流中读入一个字符,赋给字符变量 ch,遇到文件结束符,返回 0
inline istream& get(unsigned char &);
inline istream& get(signed char &);
inline istream& get(char *, int, char =‘\n’); // 从指定输入流中读 int 个字符,函数在读到比 int 少一个字符或遇到字符 char 为止(char 缺省为’\n’),并补上字符串结束符’\0’。inline istream& get(unsigned char *,int,char ='\n');
inline istream& get(signed char *,int,char ='\n');
cin.eof() // 到达文件末尾(即读到文件结束符)返回非 0 值,否则返回 0;
cin.clear() // 在读入输入流时,读到文件结束符或读入非正常字符(例如在整数输入时遇到了非数字字符)时,输入流标记置为非 0(表示不正常),clear()函数用于清除输入流的“不正常”标记。cin // 用 cin 读取字符流时,当遇到第一个空白符时,认为输入结束,并补上字符串结束符’\0’。
六十三、函数指针
1. 定义
函数的名字代表函数的入口地址。函数指针用于存放一个函数的入口地址,指向一个函数。通过函数指针,可以调用函数,这与通过函数名直接调用函数是相同的。
函数指针的定义:数据类型 (* 指针变量名)(函数形参表);
- 数据类型:此指针所指向的函数的返回值类型
- 函数指针一经定义后,可指向函数类型相同(即:函数形参的个数、形参类型、次序以及返回值类型完全相同)的不同函数
函数指针的赋值:函数指针名 = 函数名;
用函数指针变量调用函数:(* 函数指针)(实参表);
2. 函数指针作为参数
函数指针也可以做参数,以便实现函数地址的传递。