C++学习一基础

文章目录


一、C++语言基本要素

1.1.变量

变量的定义:类型名 变量名1,变量名2,……,变量名n;

1.2.C++的保留字

在这里插入图片描述

1.3.C++的数据类型

在这里插入图片描述

用sizeof运算符求变量占用字节数
sizeof(变量名)
sizeof(类型名)
能够得到某个变量或某一类型变量占用的字节数

例如:

int n1 = 10;
double f;
char c;
printf("%d,%d,%d,%d",sizeof(n1),sizeof(short),sizeof(double),sizeof(c));
//输出:4,2,8,1

1.4.有符号整数和无符号整数

short、int、long、long long 类型的变量,可以表示正数,也可以表示负数,称为有符号的整数类型。
unsigned short,unsigned int, unsigned long,unsigned long long类型的变量,只会被看作非负数,
称为无符号的整数类型。

有符号整数的表示方式:

	将最左边的位(最高位)看作“符号位”。
	符号位为0,则表示是非负数,其绝对值就
	是除符号位以外的部分;符号位为1,则表示是负数,
	其绝对值是所有位取反(0变1,1变0)后再加1。

在这里插入图片描述

将一个负整数表示为二进制的方法:

1) 设置符号位为1
2) 其余位等于绝对值取反再加1

1.5.常量

常量就是在程序运行过程中值不会发生改变,而且一眼就能看出其值的量
分类:整型,浮点型,字符型,字符串,符号常量
整型常量:

十进制整型常量:
0 123 -456 677363

十六进制整型常量,以“0x”开头:
0x123 -0x1a 0x2abcdef 0xFFA 0x100
十六进制数中,用A(a)表示10, B(b)表示11 .... F(f)表示15
(3450)10 = 0 ×100 + 5 ×101 + 4 ×102 + 3 ×103
0x2FAB = 11×160 + 10 ×161 + 15 ×162 + 2 ×163 = (12203)10
一个十六进制位,正好对应于4个二进制位 F = (1111)2

八进制整型常量,以0开头:
01 0123 -0456 0677

字符型常量:

字符型常量表示一个字符,用单引号括起来:'a' 'B' ',' '0' '9' ':'
字符型常量可用于给char 和unsigned char类型的变量赋值
char c = 'a';
unsigned char ch;
ch = '9';
字符型常量和变量都占一个字节,内部存放的是字符的ASCII编码。
ASCII编码是一个0~255的整数

在这里插入图片描述
符号常量:

1.为了阅读和修改的方便,常用一个由字母和数字组成的
符号来代表某个常量,这样的常量就叫符号常量
	#define 常量名 常量值
2.定义之后,程序中所有出现“常量名”的地方,就等价于出现的是“常量值”
3.“常量名”的命名规则和变量相同,“常量值”则写什么常量都可以
#define MAX_NUM 1000
#define UNIVERSITY_NAME "Peking University"
#define MYINT i = 5; 
4.尽量少使用数值常量,二用符号常量替代它,这样便于修改

1.6.转义字符

C++中还有一类字符常量,以’‘开头,如’\n’、‘\r’、‘\t’等,称为“转义字符”。所
谓“转义”就是指’'后面的字符被转成别的含义。
在这里插入图片描述
总结:
在这里插入图片描述


二、输入输出和基本运算

2.1.输入输出控制符

在printf和scanf中可以使用以"%"开头的控制符,指明要输入或输出的数据的类型以及格式。
在这里插入图片描述
cin,cout和scanf,printf区别:

cin,cout速度比scanf,printf慢,输入输出数据量大时用后者
一个程序里面不要同时用cin和scanf,不要同时用cout和printf

scanf表达式的值

	scanf(...)表达式的值为int,表示成功读入的变量个数。
	scanf(...) 值为EOF(即-1)则说明输入数据已经结束

cin表达式的值

	cin >> m >> n ... 表达式的值,在成功读入所有变量时为true,否则为false

2.2.运算符

赋值运算符:=、+=、-=、*=、/=、%=
算术运算符:

+-*/
求余数%
自增++
自减--

关系运算符:
比较的结果是bool类型,成立则为true,反之为false

相等==
不等!=
大于>
小于<
大于等于>=
小于等于<=

逻辑运算符:
结果是bool类型,是true或false,用于表达式的逻辑操作

&&    有一边为假,肯定是假,只有都为真,才是true||	两边都为假,才是false,否则就是true!		取反

2.3.强制类型转换运算符

类型名本身就是一个运算符,叫“强制类型转换运算符”,用于将操作数转换为指定类型
部分运算符的优先级:
在这里插入图片描述


三、分支语句和循环语句

3.1.if语句

依次计算表达式1、表达式2…只要碰到一个
表达式i为真,则执行语句组i(前面为假的表达
式对应的语句组不会被执行),后面的表达式不
再计算,后面的语句组也都不会被执行。
若所有表达式都为假,则执行语句组n

if (表达式1) {
语句组1
}
else if(表达式2) {
语句组2
}
…… //可以有多种个 else if
else if( 表达式n-1) {
语句组n-1
}
else {
语句组n
}

3.2.switch语句

“表达式”的值等于哪个“常量表达式”,
就执行相应的语句组。都不相等,则执行
default的语句组。也可以没有default分支
“常量表达式” 里面不能包含变量!

switch(表达式) { //表达式的值 必须是整数类型(int,char ……)
	case 常量表达式1//常量表达式必须是整数类型的常量(int,char…)
	语句组1
	break;
	case 常量表达式2:
	语句组2
	break;
	……
	case 常量表达式n:
	语句组n
	break;
	default:
	语句组n+1
}

3.3.for循环

for( 表达式1 ;表达式2;表达式3) {
语句组
}
  1. 计算“表达式1”。
  2. 计算“表达式2”,若其值为true,则执行“{ }”中
    的语句组,然后转到3);若为false,则不再执行“{}”
    中的语句组,for语句结束,转到5)。
  3. 计算“表达式3”。
  4. 转到2)。
  5. 从for语句后面继续往下执行程序。
    一般用于将某段代码(语句组)重复执行若干次!!!

3.4.while循环

并非到达指定次数,而是满足某条件时即停止循环,则适合用while语句来实现循环

while(表达式) {
语句组
}

while(true) {
语句组
} //死循环,可以用break跳出
  1. 判断“表达式”是否为真,如果不为真,则转4)
  2. 执行“语句组”
  3. 转1)
  4. while语句结束,继续执行while语句后面的语句。

3.5.do…while循环

如果希望循环至少要执行一次,就可以使用do…while语句
每执行一次循环后,都要判断“表达式”的值是否为真,如果真就继续循环,如果为假,就停止循环。

do {
语句组
} while(表达式);

3.6.break和continue

break:

可以出现在循环体中(for、while、do…while循环均可),其作用是跳出循环。
在多重循环的情况下,break语句只能跳出直接包含它的那一重循环

continue:

可以出现在循环体中(for、while、do…while循环均可),其作用是立即结束本次循环,并回到循环开头判断是否要进行下一次循环。
在多重循环的情况下,continue只对直接包含它的那重循环起作用。

四、数组

4.1.定义

  1. 可以用来表达类型相同的元素的集合,集合的名字就是数组名。
  2. 数组里的元素都有编号,元素的编号叫下标。通过数组名和下标,就能访问元素。
  3. 一维数组的定义方法如下:类型名 数组名[元素个数];
  4. 其中“元素个数”必须是常量或常量表达式,不能是变量,而且其值必须是正整数。元素个数也称作“数组的长度”。

4.2.初始化

  1. 在定义一个一维数组的同时,就可以给数组中的元素赋初值:
    类型名 数组名[常量表达式]={值,值……值};
  2. { }中的各数据值即为各元素的初值,值之间用逗号间隔
    例如:
int a[10]={ 0,1,2,3,4,5,6,7,8,9 };
  1. 数组大小
    表达式 T a[ N ]; //T为类型名,如char,double,int等。
    占用大小总共为 N × sizeof(T)字节的存储空间,也可以是sizeof(a)。
    上面例子的大小就是:
10*sizeof(int)
sizeof(a)

4.3.数组越界

数组元素的下标,可以是任何整数,可以是负数,也可以大于数组的元素个数。不会导致编译错误,但运行时很可能会出错!!!
例如:

int a[10];
a[-2] = 5;
a[200] = 10;
a[10] = 20;
int m = a[30];
  1. 用变量作为数组下标时,不小心会导致数组越界(变量下标值变为负数,或者太大)
  2. 可能引起意外修改其他变量的值,导致程序运行结果不正确
  3. 可能试图访问不该访问的内存区域,导致程序崩溃
  4. 数组越界的程序,用某些编译器编译后可能可以正确运行,换一个编译器编译后就运行错误

4.4.二维数组

  1. 定义N行M列的二维数组:
T a[N][M]; 
// T :类型名,如char , double, int等。
// M、N : 正整数,或值为正整数的常量表达式
  1. 每个元素都是一个类型为T的变量
  2. N×M个元素在内存里是一个挨一个连续存放的,二维数组的每一行,实际上都是一个一维数组。
  3. 数组占用了一片连续的、大小总共为 N×M×sizeof(T)字节的存储空间。
  4. 表达式“sizeof(a)”的值就是整个数组的体积,即N×M×sizeof(T)。
  5. 行下标和列下标都从0开始
  6. 访问数组元素的方法:
    数组名[行下标][列下标]
    例如: a[i][j]

例如:
int a[2][3] 在内存中的存放方式:
在这里插入图片描述


五、函数与位运算

5.1.函 数

  1. “函数” 可以将实现了某一功能,并需要反复使用的代码包装起来形成一个功能模块(即写成一个“函数”),那么当程序中需要使用该项功能时,只需写一条语句,调用实现该功能的 “函数”即可。
  2. 不同的程序员可以分别写不同的函数,拼起来形成一个大程序
  3. 如果函数不需要返回值,则“返回值类型”可以写“void”
  4. 表达式:
    返回值类型 函数名(参数1类型 参数1名称, 参数2类型 参数2名称……)
    {
    语句组(即“函数体”)
    }

5.2.函数调用和return语句

  1. 调用函数:
    函数名(参数1,参数2,……)
  2. 对函数的调用,也是一个表达式。函数调用表达式的值,由函数内部的return语句决定。return语句语法如下:
    return 返回值;
  3. return语句的功能是结束函数的执行,并将“返回值”作为结果返回。“返回值”是常量、变量或复杂的表达式均可。如果函数返回值类型为“void”,return语句就直接写:
    return ;
  4. return 语句作为函数的出口,可以在函数中多次出现。多个return语句的“返回值”可以不同。在哪个return语句结束函数的执行,函数的返回值就和哪个return语句里面的“返回值”相等。

5.3.函数的声明

  1. 一般来说函数的定义必须出现在函数调用语句之前,否则调用语句编译出错
  2. 函数的调用语句前面有函数的声明即可,不一定要有定义!参数名称可以省略。函数声明也称为“函数的原型”
    例:返回值类型 函数名(参数1类型 参数1名称, 参数2类型 参数2名称……);
int Max(int a,int b);
double Sqrt(double);
double Distance(double,double,double,double);

5.4.库函数和头文件

  1. 库函数:C/C++标准规定的,编译器自带的函数
  2. 头文件:C++编译器提供许多“头文件”,如
    iostream
    cmath
    string
  3. 头文件内部包含许多库函数的声明以及其他信息,如cin,cout的定义
    #include
  4. 即可将头文件包含到程序中,此后即可使用头文件中定义的库函数及其他信息
    示例:
    数学函数(cmath)
int abs(int x) 求整型数x的绝对值
double cos(double x)x(弧度)的余弦
double fabs(double x) 求浮点数x的绝对值
int ceil(double x) 求不小于x的最小整数
double sin(double x)x(弧度)的正弦
double sqrt(double x) 求x的平方根

字符处理函数(ctype)

int isdigit(int c) 判断 c 是否是数字字符
int isalpha(int c) 判断 c 是否是一个字母
int isalnum(int c) 判断 c 是否是一个数字或字母
int islower(int c) 判断 c 是否是一个小写字母
int islower(int c) 判断 c 是否是一个小写字母
int isupper(int c) 判断 c 是否是一个大写字母
int toupper(int c) 如果 c 是一个小写字母,则返回对应大写字母
int tolower (int c) 如果 c 是一个大写字母,则返回对应小写字母

5.5.递归

  1. 定义:一个函数,自己调用自己,就是递归
  2. 递归函数需要有终止条件,否则就会无穷递归导致程序无法终止甚至崩溃
    例如:
int F (int n) //函数返回n的阶乘
{
	if( n < 2)
		return 1; //终止条件
	else
		return n * F (n-1);
}

5.6.位运算

用于对整数类型(int,char, long 等)变量中的某一位(bit),或者若干位进行操作。比如:

  1. 判断某一位是否为1
  2. 只改变其中某一位,而保持其他位都不变。
  3. C/C++语言提供了六种位运算符来进行位运算操作:
& 按位与(双目)  //将参与运算的两操作数各对应的二进制位进行与操作,只有对应的两个二进位均为1时,结果的对应二进制位才为1,否则为0。
| 按位或(双目)	//将参与运算的两操作数各对应的二进制位进行或操作,只有对应的两个二进位都为0时,结果的对应二进制位才是0,否则为1。
^ 按位异或(双目)	//将参与运算的两操作数各对应的二进制位进行异或操作,即只有对应的两个二进位不相同时,结果的对应二进制位才是1,否则	为0。
~ 按位非(取反)(单目)	//按位非运算符“~”是单目运算符。其功能是将操作数中的二进制位0变成1,1变成0。
<< 左移(双目)//表达式:a << b的值是:将a各二进位全部左移b位后得到的值。左移时,高位丢弃,低位补0。a 的值不因运算而改变。
>> 右移(双目)
//表达式:a >> b的值是:将a各二进位全部右移b位后得到的值。右移时,移出最右边的位就被丢弃。 a 的值不因运算而改变。
//对于有符号数,如long,int,short,char类型变量,在右移时,符号位(即最高位)将一起移动,并且大多数C/C++编译器规定,如果原符号位为1,则右移时高位就补充1,原符号位为0,则右移时高位就补充0。

六、字符串

6.1.字符串的形式

  1. 用双引号括起来的字符串常量,如"CHINA" , "C++ program "。
  2. 存放于字符数组中,以‘\0’字符(ASCII码为0)结尾
  3. string对象。string是C++标准模板库里的一个类,专门用于处理字符串(略)

6.2.字符串常量

  1. 字符串常量占据内存的字节数等于字符串中字符数目加1,多出来的是结尾字符‘\0’。
  2. 字符串 “C program” 在内存中的布局:
    在这里插入图片描述
  3. 字符串的长度不包含’\0’
  4. “” 也是合法的字符串常量,称为“空串”,空串仍然会占据一个字节的存储空间,存放 ‘\0’。
  5. 如果字符串常量中包含双引号,则双引号应写为‘"’。而‘\’字符在字符串中出现时,须连写两次,变成‘\’。例如:
    cout << “He said: “I am a stu\dent.””; => He said: “I am a stu\dent.”

6.3.char数组存放字符串

  1. 包含‘\0’字符的一维char数组,就是一个字符串。其中存放的字符串即为 ‘\0’前面的字符组成。
  2. 用char数组存放字符串,数组元素个数应该至少为字符串长度+1
  3. char数组的内容,可以在初始化时设定,也可以用C++库函数进行修改,还可以用对数组元素赋值的办法任意改变其中的某个字符。
  4. 字符数组同样可以用cout、printf输出,用cin、scanf读入。用cin、scanf将字符串读入字符数组时,会自动在字符数组中字符串的末尾加上‘\0’

6.4.字符串的输入

  1. 用scanf可以将字符串读入字符数组
    scanf会自动添加结尾的’\0’
    scanf读入到空格为止
    在数组长度不足的情况下,scanf可能导致数组越界
  2. cin输入字符串的情况和scanf相同
    cin.getline(char buf[], int bufSize);
    读入一行(行长度不超过bufSize-1)或bufSize-1个字符到buf, 自动添加’\0’回车换行符不会写入buf,但是会从输入流中去掉
    gets(char buf[]);
    读入一行,自动添加’\0’回车换行符不会写入buf,但是会从输入流中去掉。可能导致数组越界!
char line[5];
scanf("%s",line); 
若输入"12345",则数组越界!

char line[5];
cin >> line; //若输入"12345",则数组越界!

char line[10];
cin.getline(line, sizeof(line));
//或 cin.getline(line,10); 读入最多9个字符到 line
cout << line;

char s[10];
while( gets(s) ) {
printf("%s\n",s);
}

6.5.字符串库函数

  1. 使用字符串函数需要 #include
  2. 字符串函数都根据’\0’来判断字符串结尾
  3. 形参为char []类型,则实参可以是char数组或字符串常量
字符串拷贝
strcpy(char dest [],char src[]); //拷贝src到dest
字符串比较大小
int strcmp(char s1[],char s2[]); //返回0则相等
求字符串长度
int strlen(char s[]); //不算结尾的'\0'
字符串拼接
strcat(char s1[],char s2[]); //s2拼接到s1后面
字符串转成大写
strupr(char s[]);
字符串转成小写
strlwr(char s[]);

七、指针

7.1.概念

  1. 每个变量都被存放在从某个内存地址(以字节为单位)开始的若干个字节中
  2. “指针”,也称作“指针变量”,大小为4个字节(或8个字节)的变量,其内容代表一个内存地址。
  3. 通过指针,能够对该指针指向的内存区域进行读写。
  4. 如果把内存的每个字节都想像成宾馆的一个房间,那么内存地址相当于就是房间号,而指针里存放的,就是房间号。

7.2.定义

类型名 * 指针变量名;
int * p; // p 是一个指针,变量 p的类型是 int *
char * pc; // pc 是一个指针, 变量 pc 的类型是 char *
float *pf; // pf 是一个指针,变量 pf 的类型是 float *

T * p ; // T 可以是任何类型的名字,比如 int, double ,char 等等。
p 的类型: T *
*p 的类型: T
通过表达式 * p,可以读写从地址p开始的 sizeof(T)个字节
*p 等价于存放在地址p处的一个 T 类型的变量
* 间接引用运算符
sizeof(T*) 4字节(64位计算机上可能8字节)

7.3.指针用法

char ch1 = 'A';
char * pc = &ch1; //使得pc指向变量ch1
& : 取地址运算符
&x : 变量x的地址(即指向x的指针)
对于类型为 T 的变量 x,&x 表示变量 x 的地址(即指向x的指针)
&x 的类型是 T *。

指针的互相赋值

不同类型的指针,如果不经过强制类型转换,不能直接互相赋值
int * pn, char * pc, char c = 0x65;
pn = pc; //类型不匹配,编译出错
pn = & c; //类型不匹配,编译出错
pn = (int * ) & c;
int n = * pn; //n值不确定
* pn = 0x12345678; //编译能过但运行可能出错

7.4.指针的作用

有了指针,就有了自由访问内存空间的手段

  1. 不需要通过变量,就能对内存直接进行操作。通过指针,程序能访问的内存区域就不仅限于变量所占据的数据区域
  2. 在C++中,用指针p指向a的地址,然后对p进行加减操作,p就能指向a后面或前面的内存区域,通过p也就能访问这些内存区域

7.5.指针的运算

  1. 两个同类型的指针变量,可以比较大小
	地址p1<地址p2, p1< p2 值为真。
	地址p1=地址p2, p1== p2 值为真
	地址p1>地址p2, p1 > p2 值为真
  1. 两个同类型的指针变量,可以相减
两个T * 类型的指针 p1和p2
p1 – p2 = ( 地址p1 – 地址 p2 ) / sizeof(T)
例:int * p1, * p2;
若 p1 指向地址 1000,p2 指向地址 600, 则
p1 – p2 = (1000 – 600)/sizeof(int) = (1000 – 600)/4 = 100
  1. 指针变量加减一个整数的结果是指针
p : T * 类型的指针
n : 整数类型的变量或常量
p+n : T * 类型的指针,指向地址:
地址p + n × sizeof(T)
n+p, p-n , *(p+n), *(p-n) 含义自明
  1. 指针变量可以自增、自减
T* 类型的指针p指向地址n
p++, ++p : p指向 n + sizeof(T)
p--, --p : p指向 n - sizeof(T)
  1. 指针可以用下标运算符“[ ]”进行运算
p 是一个 T * 类型的指针,
n 是整数类型的变量或常量
p[n] 等价于 *(p+n)
  1. 通过指针实现自由内存访问
    如何访问int型变量 a 前面的那一个字节?
int a;
char * p = (char * ) &a; // &a是 int *类型
--p;
printf("%c", * p); //可能导致运行错误
* p = 'A'; //可能导致运行错误

7.6.空指针

  1. 地址0不能访问。指向地址0的指针就是空指针
  2. 可以用“NULL”关键字对任何类型的指针进行赋值。NULL实际上就是整数0,值为NULL的指针就是空指针:
int * pn = NULL; char * pc = NULL; int * p2 = 0;
  1. 指针可以作为条件表达式使用。如果指针的值为NULL,则相当于为假,值不为NULL,就相当于为真
if(p) 等同于 if(p!=NULL)
if(!p) 等同于 if( p==NULL )

7.7.指针和数组

  1. 数组的名字是一个指针常量指向数组的起始地址
T a[N];
 a的类型是 T *
 可以用a给一个T * 类型的指针赋值
 a是编译时其值就确定了的常量,不能够对a进行赋值
  1. 作为函数形参时, T *p 和 T p[ ] 等价
void Func( int * p) { cout << sizeof(p);}
等同于
void Func( int p[]) { cout << sizeof(p);}
  1. 指针和二维数组
    如果定义二维数组:
T a[M][N];
a[i](i是整数)是一个一维数组
a[i]的类型是 T *
sizeof(a[i]) = sizeof(T) * N
a[i]指向的地址: 数组a的起始地址 + i×N×sizeof(T)
  1. 指向指针的指针
定义:
T ** p;
p是指向指针的指针,p指向的地方应该存放着一个类型为 T * 的指针
*p 的类型是 T *

案例:

#include <iostream>
using namespace std;
int main()
{
int **pp; //指向int*类型指针的指针
int * p; 
int n = 1234;
p = &n; // p指向n
pp = & p; //pp指向p
cout << *(*pp) << endl; // *pp是p, 所以*(*pp)就是n
return 0;
}
=> 1234

7.8.指针和字符串

  1. 字符串常量的类型就是 char *
  2. 字符数组名的类型也是 char *
  3. 字符串操作库函数
    在这里插入图片描述
char * strchr(const char * str,int c);
寻找字符c在字符串str中第一次出现的位置。如果找到,就返回指向该位置的char*指针;如果str中不包含字符c,则返回NULL

char * strstr(const char * str, const char * subStr);
寻找子串subStr在str中第一次出现的位置。如果找到,就返回指向该位置的指针;如果str不包含字符串subStr,则返回NULL

int stricmp(const char * s1,const char * s2);
大小写无关的字符串比较。如果s1小于s2则返回负数;如果s1等于s2,返回0;s1大于s2,返回正数。不同编译器编译出来的程序,执行stricmp的结果就可能不同。

int strncmp(const char * s1,const char * s2,int n);
比较s1前n个字符组成的子串和s2前n个字符组成的子串的大小。若长度不足n,则取整个串作为子串。返回值和strcmp类似。

char * strncpy(char * dest, const char * src,int n);
拷贝src的前n个字符到dest。如果src长度大于或等于n,该函数不会自动往dest中写入‘\0’;若src长度不足n,则拷贝src的全部内容以及结尾的‘\0’到dest。

char * strtok(char * str, const char * delim);
连续调用该函数若干次,可以做到:从str中逐个抽取出被字符串delim中的字符分隔开的若干个子串。

int atoi(char *s);
将字符串s里的内容转换成一个整型数返回。比如,如果字符串s的内容是“1234”,那么函数返回值就是1234。如果s格式不是一个整数,比如是"a12",那么返回0。

double atof(char *s);
将字符串s中的内容转换成实数返回。比如,"12.34"就会转换成12.34。如果s的格式不是一个实数 ,则返回0。

char *itoa(int value, char *string, int radix);
将整型值value以radix进制表示法写入 string

7.9.void指针

  1. 定义:void * p;
  2. 可以用任何类型的指针对 void 指针进行赋值或初始化:
double d = 1.54;
void * p = & d;
void * p1;
p1 = & d;
  1. 因 sizeof(void) 没有定义,所以对于 void * 类型的指针p, *p 无定义
    ++p, --p, p += n, p+n,p-n 等均无定义

7.10.内存操作库函数

头文件cstring中声明

  1. memset
    void * memset(void * dest,int ch,int n);
    将从dest开始的n个字节,都设置成ch。返回值是dest。ch只有最低的字节起作用。
    例:
char szName[200] = "";
memset( szName,'a',10);
cout << szName << endl;
=>aaaaaaaaaa
  1. memcpy
    void * memcpy(void * dest, void * src, int n);
    将地址src开始的n个字节,拷贝到地址dest。返回值是dest。
    例如:
将数组a1的内容拷贝到数组a2中去,结果是a2[0] = a1[0], a2[1] = 
a1[1]……a2[9] = a1[9] :
int a1[10];
int a2[10];
memcpy( a2, a1, 10*sizeof(int));

7.11.函数指针

  1. 定义:程序运行期间,每个函数都会占用一段连续的内存空间。而函数名就是该函数所占内存区域的起始地址(也称“入口地址”)。我们可以将函数的入口地址赋给一个指针变量,使该指针变量指向该函数。然后通过指针变量就可以调用这个函数。这种指向函数的指针变量称为“函数指针”。
  2. 定义形式:
类型名 (* 指针变量名)(参数类型1, 参数类型2,…);
例如:
int (*pf)(int ,char);
表示pf是一个函数指针,它所指向的函数,返回值类型应是int,该函数应有两个参数,第一个是int 类型,第二个是char类型。
  1. 使用方法
    可以用一个原型匹配的函数的名字给一个函数指针赋值。要通过函数指针调用它所指向的函数,写法为:
    函数指针名(实参表);
    例如:
#include <stdio.h>
void PrintMin(int a,int b) {
	if( a<b )
	printf("%d",a);
	else
	printf("%d",b);
}
int main() {
	void (* pf)(int ,int);
	int x = 4, y = 5;
	pf = PrintMin; 
	pf(x,y);
	return 0;
}
输出结果:
4
  1. qsort库函数
    C语言快速排序库函数:可以对任意类型的数组进行排序
void qsort(void *base, int nelem, unsigned int width, int ( * pfCompare)( const void *, const void *));
对数组排序,需要知道:
1) 数组起始地址(base)
2) 数组元素的个数(nelem)
3) 每个元素的大小(由此可以算出每个元素的地址)(width),以字节为单位
4) 元素谁在前谁在后的规则
5) pfCompare :比较函数的地址,函数指针,它指向一个“比较函数”。需要自己编写。
该比较函数应为以下形式:
	int 函数名(const void * elem1, const void * elem2);
	比较函数编写规则:
	1、如果 * elem1应该排在 * elem2前面,则函数返回值是负整数
	2、如果 * elem1和* elem2哪个排在前面都行,那么函数返回0
	3、如果 * elem1应该排在 * elem2后面,则函数返回值是正整数

例:

#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
int MyCompare( const void * elem1, const void * elem2 )
{
	unsigned int * p1, * p2;
	p1 = (unsigned int *) elem1; // “* elem1” 非法
	p2 = (unsigned int *) elem2; // “* elem2” 非法
	return (* p1 % 10) - (* p2 % 10 ); 
}
#define NUM 5
int main() 
{
	unsigned int an[NUM] = { 8,123,11,10,4 };
	qsort( an,NUM,sizeof(unsigned int),MyCompare); 
	for( int i = 0;i < NUM; i ++ )
		printf("%d ",an[i]);
	return 0;
}
输出结果:  以个位数从小到大排列
10 11 123 4 8

八、程序结构和简单算法

8.1.结构(struct)

  1. 概念

     1、在现实问题中,常常需要用一组不同类型的数据来描述一个事物。比如一个学生的学号、姓名和绩点。一个工人的姓名、性别、年龄、工资、电话......
     2、如果编程时要用多个不同类型的变量来描述一个事物,就很麻烦。当然希望只用一个变量就能代表一个“学生”这样的事物。
     3、C++允许程序员自己定义新的数据类型。因此针对“学生”这种事物,可以定义一种新名为Student的数据类型,一个Student类型的变量就能描述一个学生的全部信息。同理,还可以定义数据类型 Worker以表示工人。
    

2)定义
用“struct”关键字来定义一个“结构”,也就定义了一个新的数据类型:

struct 结构名
{
类型名 成员变量名;
类型名 成员变量名;
类型名 成员变量名;
……
};

例如:

struct Student {
unsigned ID;
char szName[20];
float fGPA;
};

3) 一般来说,一个结构变量所占的内存空间的大小,就是结构中所有成员变量大小之和。结构变量中的各个成员变量在内存中一般是连续存放的
在这里插入图片描述

  1. 结构的成员变量可以是指向本结构类型的变量的指针
struct Employee {
	string name;
	int age;
	int salary;
	Employee * next; 
};
  1. 一个结构变量的成员变量,可以完全和一个普通变量一样来使用,也可以取得其地址。使用形式:
    结构变量名.成员变量名
  2. 结构变量可以在定义时进行初始化:
struct Date {
	int year;
	int month;
	int day;
};
struct StudentEx {
	unsigned ID;
	char szName[20];
	float fGPA;
	Date birthday;
};

StudentEx stu = { 1234,"Tom",3.78,{ 1984,12,28 }};

8.2.结构数组和指针

  1. 结构数组
StudentEx MyClass [50];
StudentEx MyClass2[50] = { 
	{ 1234,"Tom",3.78,{ 1984,12,28 }},
	{ 1235,"Jack",3.25,{ 1985,12,23 }},
	{ 1236,"Mary",4.00,{ 1984,12,21 }},
	{ 1237,"Jone",2.78,{ 1985,2,28 }}
};
MyClass[1].ID = 1267;
MyClass[2].birthday.year = 1986;
int n = MyClass[2].birthday.month;
cin >> MyClass[0].szName;
  1. 指向结构变量的指针
    定义指向结构变量的指针:结构名 * 指针变量名;
    通过指针,访问其指向的结构变量的成员变量:
    指针->成员变量名
    或:
    (* 指针).成员变量名
StudentEx Stu;
StudentEx * pStu;
pStu = & Stu;
pStu->ID = 12345;
(*pStu).fGPA = 3.48;
cout << Stu.ID << endl; //输出 12345
cout << Stu.fGPA << endl; //输出 3.48

8.3.程序结构–全局变量和局部变量

  1. 定义在函数内部的变量叫局部变量(函数的形参也是局部变量)
  2. 定义在所有函数的外面的变量叫全局变量
  3. 全局变量在所有函数中均可以使用,局部变量只能在定义它的函数内部使用
#include <iostream>
using namespace std;
int n1 = 5, n2 = 10; //全局变量
void Function1()
{
	int n3 =4;	//局部变量
	n2 = 3;
}
void Function2()
{
	int n4;
	n1 = 4;
	n3 = 5; //编译出错,n3无定义
}

8.4.程序结构–静态变量

  1. 全局变量都是静态变量。局部变量定义时如果前面加了“static”关键字,则该变量也成为静态变量
  2. 静态变量的存放地址,在整个程序运行期间,都是固定不变的
  3. 非静态变量(一定是局部变量)地址每次函数调用时都可能不同,在函数的一次执行期间不变
  4. 如果未明确初始化,则静态变量会被自动初始化成全0(每个bit都是0),局部非静态变量的值则随机
#include <iostream>
using namespace std;
void Func()
{
	static int n = 4; //静态变量只初始化一次
	cout << n << endl;
	++ n;
}
int main()
{
	Func(); Func(); Func();
	return 0;
}
输出结果:
4 
5
6

静态变量应用:strtok的实现

#include <iostream>
#include <cstring>
using namespace std;
int main()
{
	char str[] ="- This, a sample string, OK.";
	//下面要从str逐个抽取出被" ,.-"这几个字符分隔的字串
	char * p = strtok (str," ,.-"); 
	while ( p != NULL) //只要p不为NULL,就说明找到了一个子串
	{
		cout << p << endl;
		p = strtok(NULL, " ,.-");
		//后续调用,第一个参数必须是NULL
	}
	return 0;
}
输出结果:
This
a
sample
string
OK

8.5.标识符作用域

  1. 变量名、函数名、类型名统称为“标识符”。一个标识符能够起作用的范围,叫做该标识符的作用域
  2. 在一个标识符的作用域之外使用该标识符,会导致“标识符没有定义”的编译错误。使用标识符的语句,必须出现在它们的声明或定义之后
  3. 在单文件的程序中,结构、函数和全局变量的作用域是其定义所在的整个文件
  4. 函数形参的作用域是整个函数
  5. 局部变量的作用域,是从定义它的语句开始,到包含它的最内层的那一对大括号“{}”的右大括号 “}”为止
  6. for循环里定义的循环控制变量,其作用域就是整个for循环
  7. 同名标示符的作用域,可能一个被另一个包含。则在小的作用域里,作用域大的那个标识符被屏蔽,不起作用。

8.6.变量生存期

  1. 所谓变量的“生存期”,指的是在此期间,变量占有内存空间,其占有的内存空间只能归它使用,不会被用来存放别的东西。
  2. 而变量的生存期终止,就意味着该变量不再占有内存空间,它原来占有的内存空间,随时可能被派做他用。
  3. 全局变量的生存期,从程序被装入内存开始,到整个程序结束。
  4. 静态局部变量的生存期,从定义它语句第一次被执行开始,到整个程序结束为止。
  5. 函数形参的生存期从函数执行开始,到函数返回时结束。非静态局部变量的生存期,从执行到定义它的语句开始,一旦程序执行到了它的作用域之外,其生存期即告终止。

8.7.简单排序

  1. 选择排序
    如果有N个元素需要排序,那么首先从N个元素中找到最小的那个(称为第0小的)放在第0个位子上(和原来的第0个位子上的元素交换位置),然后再从剩下的N-1个元素中找到最小的放在第1个位子上,然后再从剩下的N-2个元素中找到最小的放在第2个位子上……直到所有的元素都就位。
void SelectionSort(int a[] ,int size)
{
	for( int i = 0; i < size - 1; ++i ){//每次循环后将第i小的元素放好
		int tmpMin = i; 
		//用来记录从第i个到第size-1个元素中,最小的那个元素的下标
		for( int j = i+1; j < size ; ++j) {
			if( a[j] < a[tmpMin] ) 
				tmpMin = j;
		}
//下面将第i小的元素放在第i个位子上,并将原来占着第i个位子的元素挪到后面
		int tmp = a[i];
		a[i] = a[tmpMin]; 
		a[tmpMin] = tmp; 
	}
}
  1. 插入排序

     1、将整个数组a分为有序的部分和无序的两个部分。前者在左边,后者在右边。
     2、开始有序的部分只有a[0],其余都属于无序的部分
     3、每次取出无序部分的第一个(最左边)元素,把它加入到有序部分。假设插入到合适位置p, 则原p位置及其后面的有序部分元素,都向右移动一个位子。有序的部分即增加了一个元素。
     4、直到无序的部分没有元素
    
void InsertionSort(int a[] ,int size)
{
	for(int i = 1;i < size; ++i ) { 
		//a[i]是最左的无序元素,每次循环将a[i]放到合适位置
		for(int j = 0; j < i; ++j) 
			if( a[j]>a[i]) { 
				//要把a[i]放到位置j,原下标j到 i-1的元素都往后移一个位子
				int tmp = a[i];
				for(int k = i; k > j; --k)
					a[k] = a[k-1];
				a[j] = tmp;
				break;
			}
	}
}
  1. 冒泡排序

     1、将整个数组a分为有序的部分和无序的两个部分。前者在右,后者在左边。
     2、开始,整个数组都是无序的。有序的部分没有元素。
     3、每次要使得无序部分最大的元素移动到有序部分第一个元素的左边。移动的方法是:依次比较相邻的两个元素,如果前面的比后面的大,就交换他们的位置。这样,大的元素就像水里气泡一样不断往上浮。移动结束有序部分增加了一个元素。
     4、直到无序的部分没有元素
    
void BubbleSort(int a[] ,int size)
{
	for(int i = size-1;i > 0; --i ) { 
		//每次要将未排序部分的最大值移动到下标i的位置
		for(int j = 0; j < i; ++j) //依次比较相邻的两个元素
			if( a[j] > a[j+1]) {
				int tmp = a[j];
				a[j] = a[j+1];
				a[j+1] = tmp;
		}
	}
}
  1. 简单排序的效率

     上面3种简单排序算法,都要做 n2量级次数的比较(n是元素个数)!
     好的排序算法,如快速排序,归并排序等,只需要做n*log2n量级次数的比较!
    

九、文件读写

9.1.文件读写过程

  1. 文件读写相关函数在头文件 cstdio (或stdio.h)中声明**#include cstdio**
  2. fopen函数打开文件, 返回 FILE * 指针,指向和文件相关的一个FILE变量(FILE 是在cstdio中定义的struct)
  3. fscanf、fread,fgets,fgetc 等函数读文件,fprintf, fwrite,fputs,fputc等函数写文件,都需要通过fopen返回的FILE *指针进行
  4. 文件读写结束后,一定要fclose关闭文件!!!否则可能导致写入文件的数据没被保存,或者无法打开其它文件

9.2.打开文件的函数

FILE * fopen(char * filename, char * mode)

  1. 以mode模式打开文件 filename,如果打开失败,返回NULL如果成功,返回一个 FILE * 指针,以后对文件的操作均通过该指针进行。

  2. FILE 是一个struct,记录被打开文件的各种信息。

  3. 打开失败的原因:文件不存在,文件名不合法,试图以写方式打开只读文件,没有权限…

  4. 打开文件的模式

     fopen函数mode参数取值:
     "r": 读模式。打开文件用于读取信息。
     "w": 写模式。创建打开文件用于写入信息。如果filename已经存在,则原文件立即丢失。
     "a": 添加模式。打开已有文件,在尾部添加新内容。如果文件不存在,则创建之。所有写入操作的效果都是往尾部添加内容。
     "r+": 读写模式。打开文件,可读可写。如果文件本来存在,则原内容不会丢失;如果文件本来不存在,打开失败。
     打开模式中加"b"即指定为二进制方式打开,如"rb","wb","ab","r+b"否则称为以文本方式打开。
     
     注意:
     Linux,Unix下的换行符号:‘\n’ (ASCII码: 0x0a)
     Windows 下的换行符号:‘\r\n’ (ASCII码: 0x0d0a)
     Mac OS下的换行符号: ‘\r’ (ASCII码:0x0d)
    

9.3.文件名的绝对路径和相对路径

  1. 绝对路径:
"c:\\tmp\\mydir\\some.txt"
  1. 相对路径:
"\\tmp\\mydir\\some.txt"
当前盘符的根目录下的tmp\dir\some.txt
"tmp\\mydir\\some.txt"
当前文件夹的tmp子文件夹里面的…..
"..\\tmp\\mydir\\some.txt"
当前文件夹的父文件夹下面的tmp子文件夹里面的…..
"..\\..\\tmp\\mydir\\some.txt"
当前文件夹的父文件夹的父文件夹下面的tmp子文件夹里面的…..

默认情况下,程序运行时的当前文件夹就是程序可执行文件所在的文件夹

9.4.文本文件读写

  1. int fscanf(FILE * fp, char * fmt, par1, par2 …);
    除第一个参数外,其余参数格式和用法同scanf。和scanf的区别在于不是从键盘读入数据,而是从fp关联的文件中读入数据
  2. int fprintf (FILE * fp, char * fmt, par1, par2 …);
    除第一个参数外,其余参数格式和用法同 printf。和printf 的区别在于不是将文字输出到屏幕,而是输出到fp关联的文件。
    例如:
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int a[1000];
int main() { 
	FILE * fpIn = fopen("in.txt","r");
	if ( fpIn == NULL)
		return 0; //文件打开失败
	FILE * fpOut = fopen("out.txt","w");
	int n = 0;
	while( fscanf(fpIn,"%d",a+n) != EOF) 
		++n;
	fclose(fpIn);
	sort(a,a+n);
	for (int i = 0;i < n; ++i)
		fprintf(fpOut,"%d ",a[i]);
	fclose(fpOut);
	return 0;
}
  1. char * fgets ( char * str, int num, FILE * fp);
    读取若干字符到str,碰到以下三种情况之一读取就结束:
    1)已经读入 (num-1) 个字符
    2)碰到了换行符(换行符会被读入)
    3)文件全部读完
    会自动往读入的最后一个字符后面加’\0’
    读入成功则返回str,否则返回NULL
    例如:
#include <iostream>
#include <cstdio>
using namespace std;
int main()
{
	char str[1000];
	FILE * fp = fopen("tmp.txt","r");
	while( fgets(str,990,fp)) 
		printf(str);
	fclose(fp);
	return 0;
}
  1. int fputs (char * str, FILE * fp);
    将字符串str输出到文件fp。若不成功,返回EOF(一般就是-1)

9.5.二进制文件读写

  1. 文件的读写指针

     1、每个打开的文件都有一个“读写指针”
     2、读写指针标识文件读写操作的当前位置, 该指针在哪里,读写操作就在哪里进行。
     3、以"a"模式打开文件,读写指针指向文件末尾。其它情况读写指针指向文件开头
     4、"读写指针"和fopen返回的FILE * 指针不是一回事
    
  2. int fseek ( FILE * fp, long long offset, int origin );

     将文件读写指针定位到距离origin位置offset字节处
     origin取值:
     SEEK_SET: 文件开头(偏移量0处),此时offset为非负数
     SEEK_END: 文件末尾,此时offset为非正数
     SEEK_CUR: 当前文件读写指针位置,此时offset可正可负
    
  3. unsigned int fread ( void * ptr, unsigned int size, unsigned int count, FILE * fp );

     1、进行二进制读文件
     2、将文件读写指针指向的地方的 size*count 个字节内容,读入到内存地址ptr,然后将文件读写指针向后移动size*count字节。
     3、返回值为: 成功读取的字节数/size
    
  4. int fgetc ( FILE * fp );

     1、进行二进制读文件
     2、读取文件fp中的下一个字节。如果文件已经读完,返回EOF(-1)
     3、注意必须用int存放函数返回值,不可用char存放返回值。否则函数返回 -1 (0xffffffff)时无法区分是读取了一个ASCII编码为0xff的字符,还是文件已经读完。
    
  5. unsigned int fwrite ( void * ptr, unsigned int size, unsigned int count, FILE * fp );

     1、二进制写文件
     2、将内存地址ptr处的size*count个字节内容,写入到文件读写指针指向的地方,然后将文件读写指针向后移动size*count字节
     3、返回值为: 成功写入的字节数/size
    
  6. int fputc ( int c, FILE * fp );

     1、二进制写文件
     2、将 c 的低8位写入文件fp。
    

案例:在文件中写入和读取一个整数

#include <iostream>
#include <cstdio>
using namespace std;
int main() { 
	FILE * fp = fopen("some.dat","wb"); 
	int x,y;
	scanf("%d",&x);
	fwrite(&x,sizeof(int),1,fp);
	fclose(fp);
	fp = fopen("some.dat","rb");
	fread(&y,sizeof(int),1,fp);
	fclose(fp);
	printf("%d",y);
	return 0;
}
//从键盘输入几个学生的姓名和成绩,并以二进制文件形式保存
#include <iostream>
#include <cstdio>
using namespace std;
struct Student {
		char name[20];
		int score;
};
int main() {
	Student s;
	FILE * fp = fopen("c:\\tmp\\students.dat","wb");
	while (scanf("%s%d",s.name,&s.score) != EOF) 
		fwrite(&s, sizeof(s),1,fp);
	fclose(fp);

	//打印信息
	FILE * fp = fopen("c:\\tmp\\students.dat","rb");
	while(fread(&s,sizeof(s),1,fp) == 1 ) 
		printf("%s %d\n",s.name,s.score);
	fclose(fp);
	return 0;
}
  1. 优点

     用二进制文件存学生信息比用文本方式存的好处:
     1)可能节约空间
     2)便于快速读取、改写单个学生信息
     3)便于排序、查找学生信息
    
  2. 注意事项

     用"r+b"打开文件既读又写时,如果做了读操作,则做写操作之前一定要用fseek重新定位文件读写指针,哪怕此时指针位置
     好是正确的;同理如果做了写操作,则再做读操作之前也一定要用fseek重新定位文件读写指针。否则读写都不正确,可能
     造成文件数据混乱、丢失
    

案例:

#include <iostream>
#include <cstdio>
using namespace std;
struct Student {
		char name[20];
		int score;
};
int main()
{
	Student s;
	FILE * fp = fopen("c:\\tmp\\students.dat","r+b");
	if( !fp) {
		printf("error\n"); return 0;
	}
	fseek(fp, 2 * sizeof(s),SEEK_SET); //定位读写指针到第三个记录
	fwrite("Mike",1,5,fp); 
	fseek(fp,0,SEEK_SET); //定位读写指针到文件开头
	while( fread(&s,sizeof(s),1,fp) == 1) 
		printf("%s %d\n",s.name,s.score);
	fclose(fp);
	return 0;
}

案例:文件拷贝程序示例

#include <iostream>
#include <cstdio>
using namespace std;
int main(int argc, char * argv[])
{
if( argc != 3 ) {
	printf("File name missing!\n");
	return 0;
	}
	FILE * fpIn = fopen(argv[1],"rb");//打开文件用于读
	if( !fpIn ) {
		printf("Source file open error.\n");
		return 0;
	}
	FILE * fpOut = fopen(argv[2],"wb"); //打开文件用于写
	if( !fpOut) {
		printf("New file open error.\n");
		fclose(fpIn); //打开的文件一定要关闭
		return 0;
	}
	int c; //必须是int
	while( (c = fgetc(fpIn)) != EOF) //每次读取一个字节
		fputc(c,fpOut); //每次写入一个字节
	//上面三行也可以替换成如下三行代码
	char buf[4096];
	int n;
	while( n = fread(buf,1,4096,fpIn)) 
	fwrite(buf,1,n,fpOut);
	
	fclose(fpIn);
	fclose(fpOut);
	return 0;
}

十、STL

10.1.STL概述

  1. STL: (Standard Template Library) 标准模板库
  2. 包含一些常用的算法如排序查找,还有常用的数据结构如可变长数组、链表、字典等。
  3. 使用方便,效率较高
  4. 要使用其中的算法,需要#include

10.2.排序算法 sort

  1. 用法一
    sort(数组名+n1,数组名+n2);

     对基本类型的数组从小到大排序
     n1和n2都是int类型的表达式,可以包含变量
     如果n1=0,则 + n1可以不写
     将数组中下标范围为[n1,n2)的元素从小到大排序。下标为n2的元素不在排序区间内
    

例如:

int a[] = {15,4,3,9,7,2,6};
sort(a,a+7); //对整个数组从小到大排序
int a[] = {15,4,3,9,7,2,6};
sort(a,a+3); // 结果:{3,4,15,9,7,2,6}
int a[] = {15,4,3,9,7,2,6};
sort(a+2,a+5); //结果:{15,4,3,7,9,2,6}
  1. 用法二
    对元素类型为T的基本类型数组从大到小排序:
    sort(数组名+n1,数组名+n2,greater());
int a[] = {15,4,3,9,7,2,6};
sort(a+1,a+4,greater<int>()); // 结果:{15,9,4,3,7,2,6}
  1. 用法三
    用自定义的排序规则,对任何类型T的数组排序
    sort(数组名+n1,数组名+n2,排序规则结构名());
    排序规则结构的定义方式:
struct 结构名
{
	bool operator()( const T & a1,const T & a2) const {
	//若a1应该在a2前面,则返回true。
	//否则返回false。
	}
};

注意:

排序规则返回 true,意味着 a1 必须在 a2 前面
返回 false,意味着 a1 并非必须在 a2 前面
排序规则的写法,不能造成比较 a1,a2 返回 true 比较 a2,a1 也返回 true 
否则sort会 runtime error
比较 a1,a2 返回 false 比较 a2,a1 也返回 false,则没有问题

案例1:

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
struct Rule1 //按从大到小排序
{
	bool operator()( const int & a1,const int & a2) const {
		return a1 > a2;
	}
};
struct Rule2 //按个位数从小到大排序
{
	bool operator()( const int & a1,const int & a2) const {
		return a1%10 < a2%10;
	}
};

void Print(int a[],int size) {
	for(int i = 0;i < size;++i) 
		cout << a[i] << "," ;
	cout << endl;
}
int main()
{
	int a[] = { 12,45,3,98,21,7};
	sort(a,a+sizeof(a)/sizeof(int)); //从小到大
	cout << "1) "; Print(a,sizeof(a)/sizeof(int));
	sort(a,a+sizeof(a)/sizeof(int),Rule1()); //从大到小
	cout << "2) "; Print(a,sizeof(a)/sizeof(int));
	sort(a,a+sizeof(a)/sizeof(int),Rule2()); //按个位数从小到大
	cout << "3) "; Print(a,sizeof(a)/sizeof(int));
	return 0;
}
答案
1) 3,7,12,21,45,98,
2) 98,45,21,12,7,3,
3) 21,12,3,45,7,98,

案例2:

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
struct Student {
		char name[20];
		int id;
		double gpa;
};
Student students [] = { 
		{"Jack",112,3.4},{"Mary",102,3.8},{"Mary",117,3.9},
		{"Ala",333,3.5},{"Zero",101,4.0}};

struct StudentRule1 { //按姓名从小到大排
		bool operator() (const Student & s1,const Student & s2) const {
			if( stricmp(s1.name,s2.name) < 0)
				return true;
			return false;
		}
};
struct StudentRule2 { //按id从小到大排
		bool operator() (const Student & s1,const Student & s2) const {
			return s1.id < s2.id;
		}
};
struct StudentRule3 {//按gpa从高到低排
		bool operator() (const Student & s1,const Student & s2) const {
			return s1.gpa > s2.gpa;
		}
};

void PrintStudents(Student s[],int size){
	for(int i = 0;i < size;++i)
		cout << "(" << s[i].name << "," 
			<< s[i].id <<"," << s[i].gpa << ") " ;
	cout << endl;
}

int main()
{
	int n = sizeof(students) / sizeof(Student);
	sort(students,students+n,StudentRule1()); //按姓名从小到大排
	PrintStudents(students,n);
	sort(students,students+n,StudentRule2()); //按id从小到大排
	PrintStudents(students,n);
	sort(students,students+n,StudentRule3()); //按gpa从高到低排
	PrintStudents(students,n);
	return 0;
}
(Ala,333,3.5) (Jack,112,3.4) (Mary,102,3.8) (Mary,117,3.9) (Zero,101,4)
(Zero,101,4) (Mary,102,3.8) (Jack,112,3.4) (Mary,117,3.9) (Ala,333,3.5)
(Zero,101,4) (Mary,117,3.9) (Mary,102,3.8) (Ala,333,3.5) (Jack,112,3.4)

10.3.二分查找算法

STL提供在排好序的数组上进行二分查找的算法:
binary_search
lower_bound
upper_bound

  1. binary_search进行二分查找(用法一)
    binary_search(数组名+n1,数组名+n2,值);

     在从小到大排好序的基本类型数组上进行二分查找
     n1和n2都是int类型的表达式,可以包含变量
     如果n1=0,则 + n1可以不写
     查找区间为下标范围为[n1,n2)的元素,下标为n2的元素不在查找区间内
     在该区间内查找"等于"值”的元素,返回值为true(找到)或false(没找到)
     "等于"的含义: a 等于 B <=> a < b和b < a都不成立
    
  2. 用binary_search进行二分查找(用法二)
    binary_search(数组名+n1,数组名+n2,值,排序规则结构名());

     在用自定义排序规则排好序的、元素为任意的T类型的数组中进行二分查找
     n1和n2都是int类型的表达式,可以包含变量
     如果n1=0,则 + n1可以不写
     查找区间为下标范围为[n1,n2)的元素,下标为n2的元素不在查找区间内
     在该区间内查找"等于"值的元素,返回值为true(找到)或false(没找到)
     查找时的排序规则,必须和排序时的规则一致!
     "等于"的含义: a 等于 b <=> "a必须在b前面"和"b必须在a前面"都不成立
    

案例:

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
struct Rule //按个位数从小到大排
{
		bool operator()( const int & a1,const int & a2) const {
			return a1%10 < a2%10;
		}
};
void Print(int a[],int size) {
	for(int i = 0;i < size;++i) {
		cout << a[i] << "," ;
	}
	cout << endl;
}
int main() {
	int a[] = { 12,45,3,98,21,7};
	sort(a,a+6);
	Print(a,6);
	cout <<"result:"<< binary_search(a,a+6,12) << endl;
	cout <<"result:"<< binary_search(a,a+6,77) << endl;
	sort(a,a+6,Rule()); //按个位数从小到大排
	Print(a,6);
	cout <<"result:"<< binary_search(a,a+6,7) << endl;
	cout <<"result:"<< binary_search(a,a+6,8,Rule()) << endl;
	return 0;
}
答案:
3,7,12,21,45,98,
result:1
result:0
21,12,3,45,7,98,
result:0
result:1
  1. 用lower_bound二分查找下界(用法一)
    T * lower_bound(数组名+n1,数组名+n2,值);

     在对元素类型为T的从小到大排好序的基本类型的数组中进行查找
     返回一个指针 T * p;
     *p 是查找区间里下标最小的,大于等于"值" 的元素。如果找不到,p指向下标为n2的元素
    
  2. 用lower_bound二分查找下界(用法二)
    T * lower_bound(数组名+n1,数组名+n2,值,排序规则结构名());

     在元素为任意的T类型、按照自定义排序规则排好序的数组中进行查找
     返回一个指针 T * p;
     *p 是查找区间里下标最小的,按自定义排序规则,可以排在"值"后面的元素。如果找
     不到,p指向下标为n2的元素
    
  3. 用upper_bound二分查找上界(用法一)
    T * upper_bound(数组名+n1,数组名+n2,值);

     在元素类型为T的从小到大排好序的基本类型的数组中进行查找
     返回一个指针 T * p;
     *p 是查找区间里下标最小的,大于"值"的元素。如果找不到,p指向下标为n2的元素
    
  4. 用upper_bound二分查找上界(用法二)
    T * upper_bound(数组名+n1,数组名+n2,值,排序规则结构名());

     在元素为任意的T类型、按照自定义排序规则排好序的数组中进行查找
     返回一个指针 T * p;
     *p 是查找区间里下标最小的,按自定义排序规则,必须排在"值"后面的元素。如果找
     不到,p指向下标为n2的元素
    

案例:

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
struct Rule 
{
	bool operator()( const int & a1,const int & a2) const {
		return a1%10 < a2%10;
	}
};
void Print(int a[],int size) {
	for(int i = 0;i < size;++i) {
		cout << a[i] << "," ;
	}
	cout << endl;
}

#define NUM 7 
int main()
{
	int a[NUM] = { 12,5,3,5,98,21,7};
	sort(a,a+NUM);
	Print(a,NUM); // => 3,5,5,7,12,21,98,
	int * p = lower_bound(a,a+NUM,5);
	cout << *p << "," << p-a << endl; //=> 5,1
	p = upper_bound(a,a+NUM,5);
	cout << *p << endl; //=>7
	cout << * upper_bound(a,a+NUM,13) << endl; //=>21
	sort(a,a+NUM,Rule());
	Print(a,NUM); //=>21,12,3,5,5,7,98,
	cout << * lower_bound(a,a+NUM,16,Rule()) << endl; // => 7
	cout << lower_bound(a,a+NUM,25,Rule()) - a<< endl; // => 3
	cout << upper_bound(a,a+NUM,18,Rule()) - a << endl; // => 7
	if( upper_bound(a,a+NUM,18,Rule()) == a+NUM)
	cout << "not found" << endl; //=> not found
	cout << * upper_bound(a,a+NUM,5,Rule()) << endl; // =>7
	cout << * upper_bound(a,a+NUM,4,Rule()) << endl; // =>5
	return 0;
}

10.4.STL中的平衡二叉树

1) 概念

	1、有时需要在大量增加、删除数据的同时,还要进行大量数据的查找
	2、希望增加数据、删除数据、查找数据都能在 log(n)复杂度完成
	3、排序+二分查找显然不可以,因加入新数据就要重新排序
	4、可以使用“平衡二叉树”数据结构存放数据,体现在STL中,就是以下四种“排序容器” :
	multiset set multimap map

2) multiset(有序,可重复)

用法:multiset<T> st;

	定义了一个multiset变量st,st里面可以存放T类型的数据,并且能自动排序。开始st为空
	排序规则:表达式 “a < b” 为true,则 a 排在 b 前面
	可用 st.insert添加元素,st.find查找元素,st.erase删除元素,复杂度都是 log(n)
multiset 上的迭代器:`multiset<T>::iterator p;`

	p是迭代器,相当于指针,可用于指向multiset中的元素。访问multiset中的元素要通过迭代器。
	与指针的不同:
	multiset上的迭代器可 ++ ,--, 用 != 和 == 比较,不可比大小,不可加减整数,不可相减
	st.begin() 返回值类型为 multiset<T>::iterator, 是指向st中的头一个元素的迭代器
	st.end() 返回值类型为 multiset<T>::iterator, 是指向st中的最后一个元素后面的迭代器
	对迭代器 ++ ,其就指向容器中下一个元素,-- 则令其指向上一个元素
	
案例1:
#include <iostream>
#include <cstring>
#include <set> //使用multiset和set需要此头文件
using namespace std;
int main()
{
	multiset<int> st; 
	int a[10]={1,14,12,13,7,13,21,19,8,8 };
	for(int i = 0;i < 10; ++i)
		st.insert(a[i]); //插入的是a [i]的复制品
	multiset<int>::iterator i; //迭代器,近似于指针
	for(i = st.begin(); i != st.end(); ++i) 
		cout << * i << ","; 
	cout << endl;		//输出:1,7,8,8,12,13,13,14,19,21,
	i = st.find(22); //查找22,返回值是迭代器
	if( i == st.end()) //找不到则返回值为 end()
		cout << "not found" << endl;
	st.insert(22); //插入 22
	i = st.find(22);
	if( i == st.end())
		cout << "not found" << endl;
	else
		cout << "found:" << *i <<endl; 
	//找到则返回指向找到的元素的迭代器
	i = st.lower_bound(13);
	//返回最靠后的迭代器 it,使得[begin(),it)中的元素
	//都在 13 前面 ,复杂度 log(n)
	cout << * i << endl;
	i = st.upper_bound(8);
	//返回最靠前的迭代器 it,使得[it,end())中的元素
	//都在 8 后面,复杂度 log(n)
	cout << * i << endl;
	st.erase(i); //删除迭代器 i 指向的元素,即12
	for(i = st.begin(); i != st.end(); ++i) 
		cout << * i << ",";
	return 0;
}
案例2:
#include <iostream>
#include <cstring>
#include <set>
using namespace std;
struct Rule1 {
		bool operator()( const int & a,const int & b) const { 
			return (a%10) < (b%10); 
		}//返回值为true则说明a必须排在b前面  按照个位数从小到大排序
};
int main() {
	multiset<int,greater<int> > st; //排序规则为从大到小
	int a[10]={1,14,12,13,7,13,21,19,8,8 };
	for(int i = 0;i < 10; ++i)
		st.insert(a[i]);
	multiset<int,greater<int> >::iterator i; 
	for(i = st.begin(); i != st.end(); ++i) 
		cout << * i << ",";
	cout << endl;		//输出:21,19,14,13,13,12,8,8,7,1,
	multiset<int,Rule1 > st2;
	//st2的元素排序规则为:个位数小的排前面
	for(int i = 0;i < 10; ++i)
		st2.insert(a[i]);
	multiset<int,Rule1>::iterator p; 
	for(p = st2.begin(); p != st2.end(); ++p) 
		cout << * p << ",";
	cout << endl;		//输出:1,21,12,13,13,14,7,8,8,19,
	p = st2.find(133);
	cout << * p << endl;	//13
	return 0;
}
find(x): 在排序容器中找一个元素y,使得“x必须排在y前面”和 “y必须排在x前面”都不成立

3) set (有序,不可重复)

set和multiset的区别在于容器里不能有重复元素
set插入元素可能不成功(可能是重复元素)
案例:

#include <iostream>
#include <cstring>
#include <set>
using namespace std;
int main()
{
	set<int> st;
	int a[10] ={ 1,2,3,8,7,7,5,6,8,12 };
	for(int i = 0;i < 10; ++i)
		st.insert(a[i]);
	cout << st.size() << endl; //输出:8
	set<int>::iterator i;
	for(i = st.begin(); i != st.end(); ++i)
		cout << * i << ","; //输出:1,2,3,5,6,7,8,12,
	cout << endl;
	pair<set<int>::iterator, bool> result = st.insert(2);
	if( ! result.second ) //条件成立说明插入不成功
		cout << * result.first <<" already exists." << endl;
	else
		cout << * result.first << " inserted." << endl;
	return 0;
}
pair<set<int>::iterator, bool>
等同于
struct {
	set<int>::iterator first;
	bool second;
};

4)pair模板的用法

pair<T1,T2>类型等价于:
struct {
T1 first;
T2 second;
};
例如:pair<int, double> a; 
等价于:
struct {
int first;
double second;
} a;
a.first = 1;
a.second = 93.93;

10.5.multimap

用法:multimap<T1,T2> mp;

multimap容器里的元素,都是pair形式的
则mp里的元素都是如下类型:
struct {
T1 first; //关键字
T2 second; //值
};
multimap中的元素按照first排序,并可以按first进行查找
缺省的排序规则是 "a.first < b.first" 为true,则a排在b前面

案例:

一个学生成绩录入和查询系统,接受以下两种输入:
Add name id score
Query score
name是个不超过16字符的字符串,中间没有空格,代表学生姓名。id
是个整数,代表学号。score是个整数,表示分数。学号不会重复,分数
和姓名都可能重复。
两种输入交替出现。第一种输入表示要添加一个学生的信息,碰到这
种输入,就记下学生的姓名、id和分数。第二种输入表示要查询,碰到这
种输入,就输出已有记录中分数比score低的最高分获得者的姓名、学号
和分数。如果有多个学生都满足条件,就输出学号最大的那个学生的信
息。如果找不到满足条件的学生,则输出“Nobody”
输入样例:
Add Jack 12 78
Query 78
Query 81
Add Percy 9 81
Add Marry 8 81
Query 82
Add Tom 11 79
Query 80
Query 81
输出样例:
Nobody
Jack 12 78
Percy 9 81
Tom 11 79
Tom 11 79
#include <iostream>
#include <map> //使用multimap和map需要包含此头文件
#include <cstring>
using namespace std;
struct StudentInfo {
		int id;
		char name[20];
};
struct Student {
		int score;
		StudentInfo info; 
};
typedef multimap<int,StudentInfo> MAP_STD; 
// 此后 MAP_STD 等价于 multimap<int,StudentInfo>
// typedef int * PINT;
// 则此后 PINT 等价于 int *。 即 PINT p; 等价于 int * p;
int main() {
	MAP_STD mp;
	Student st;
	char cmd[20];
	while( cin >> cmd ) {
		if( cmd[0] == 'A') {
		cin >> st.info.name >> st.info.id >> st.score ;
			mp.insert(make_pair(st.score,st.info ));
		} //make_pair生成一个 pair<int,StudentInfo>变量
		//其first 等于 st.score, second 等于 st.info
		else if( cmd[0] == 'Q' ){
				int score;
				cin >> score;
				MAP_STD::iterator p = mp.lower_bound (score);
				if( p!= mp.begin()) { 
					--p;
					score = p->first; //比要查询分数低的最高分
					MAP_STD::iterator maxp = p; 
					int maxId = p->second.id; 
					for(; p != mp.begin() && 
						p->first == score; --p) {
						//遍历所有成绩和score相等的学生
						if( p->second.id > maxId ) {
							maxp = p;
							maxId = p->second.id ;
						}
		}
		if( p->first == score) { 
		//如果上面循环是因为 p == mp.begin() 而终止,则p指向的元素还要处理
			if( p->second.id > maxId ) {
				maxp = p;
				maxId = p->second.id ;
			}
		}
		cout << maxp->second.name << " " 
			<< maxp->second.id << " " 
			<< maxp->first << endl;
		}
		//lower_bound的结果就是 begin,说明没人分数比查询分数低
		else cout << "Nobody" << endl;
		}
	}
	return 0;
}

10.6.map

和multimap区别在于:
不能有关键字重复的元素
可以使用 [] ,下标为关键字,返回值为first和关键字相同的元素的second
插入元素可能失败

案例:

#include <iostream>
#include <map> 
#include <string>
using namespace std;
struct Student {
		string name;
		int score;
};
Student students[5] = { 
{"Jack",89},{"Tom",74},{"Cindy",87},{"Alysa",87},{"Micheal",98}}; 
typedef map<string,int> MP;
int main()
{
	MP mp; 
	for(int i = 0;i < 5; ++i)
		mp.insert(make_pair(students[i].name,students[i].score));
	cout << mp["Jack"] << endl; // 输出 89
	mp["Jack"] = 60; //修改名为"Jack"的元素的second
	for(MP::iterator i = mp.begin(); i != mp.end(); ++i)
		cout << "(" << i->first << "," << i->second << ") ";
	//输出:(Alysa,87) (Cindy,87) (Jack,60) (Micheal,98) (Tom,74)
	cout << endl;
	Student st;
	st.name = "Jack";
	st.score = 99;
	pair<MP::iterator, bool> p =
		mp.insert(make_pair(st.name,st.score));
	if( p.second ) 
		cout << "(" << p.first->first << "," << p.first->second << ") inserted" <<endl;
	else
		cout << "insertion failed" << endl; //输出此信息
	mp["Harry"] = 78; //插入一元素,其first为"Harry",然后将其second改为78 
	MP::iterator q = mp.find("Harry");
	cout << "(" << q->first << "," << q->second <<")" <<endl;//输出 (Harry,78)
	return 0;
}

总结

本总结所有内容均出自北京大学郭炜老师的程序设计与算法(一)C语言程序设计PPT中,只为查阅方便。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值