一、C++、QT与嵌入式的那些事o,0’
C++:与QT配合搭建图形化开发,阅读linux内核源码打基础
QT:在嵌入式领域中进行图形化开发
市场上的嵌入式设备:
1、单片机:RTOS(FreeRTOS / RTThread)
跑界面:mini GUI (C/C++) LVGL(C语言)
2、高端ARM开发板:安卓
跑界面:专门做安卓应用的开发
3、高端ARM开发板:Linux
跑界面:QT
国家大力推国产化:
芯片:龙芯、飞腾、兆易创新
OS :鸿蒙、澎湃、优麒麟、红旗
在国产芯片上移植国产的操作系统就是嵌入式工程师的工作。
国产操作系统以Linux为基础,因此图形化开发也需要我们。
-----------------------------------------------------------------------------------------
要给一个指定的地址存储整数,需要先将地址强转为地址类型
如:
*(int *)(0x12345678) = 100;
*(volatile unsigned int *)(0x12345678) = 100;(常用)
volatile:防止编译器优化(编译器优化指的是,当编译器发现你初始化了一个值,但在接下来的语句中并没有进行操作,则编译器认为你这种行为是没有实际意义的,所以给你优化掉了,就是不执行了)
不可:
0x12345678 = 100 (编译器理解为要对整型数进行赋值,报错)
*(0x12345678) = 100 (编译器理解为要对一个整型数进行取值,报错)
-----------------------------------------------------------------------------------------
1、职位匹配
(1)、C++开发工程师
(2)、嵌入式应用开发(C/C++)
(3)、嵌入式QT开发工程师
2、主要学习内容
(1)、C到C++过渡
(2)、类和对象(C++如何创建类、创建对象、如何进行封装?)
(3)、继承和多态
(4)、模板(泛型编程)
(5)、文件IO
(6)、异常处理
(7)、多线程
(8)、STL模板库(数据结构和算法)
3、学习环境
开发环境 Linux平台
编辑器 Vim、VsCode
编译器 g++(和gcc一样是GUN的编译器)
文件后缀 xxx.cpp(C Plus Plus)
二、从C到C++
1、头文件
【c】 #include <stdio.h>
【c++】 #include <iostream> / #include <stdlib>
注意:C++在包换头文件时不需要写“.h”,如果想要包含“stdlib.h”,只需要写成#include <stdlib>
主要因为C++中有一个概念“命名空间”(namespace)
早期标准库将所有的功能在全局域中实现,声明在.h文件使用的时候需要包含对应的头文件,后来C++标准引入了命名空间,就把C++的标准库的定义实现都放在了命名空间中。
-----------------------------------------------------------------------------------------
包含头文件时,尖括号<>与双引号""的区别
主要是搜索路径不同
双引号#include“”格式:我们常常引用非标准库的头文件,即我们自己写的头文件,编译器从用户的工作目录开始搜索
1)在当前源文件所在的工作目录中进行查找
2)在编译器设置的头文件查找路径,编译器有默认的头文件查找路径(可以在编译时,使用-l显式指定搜索路径)
3)系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径
尖括号#include<>格式: 我们常常引用标准库头文件,编译器从标准库目录开始搜索
1)在编译器设置的头文件查找路径,编译器有默认的头文件查找路径(可以在编译时,使用-l显式指定搜索路径)
2)系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径
-----------------------------------------------------------------------------------------
2、命名空间(namespace)
将变量名(一般是全局的)(函数名、类型名)限制到一个空间(命名空间)
-----------------------------------------------------------------------------------------
变量的三个关注点(作用域,存储空间,生命周期)
全局变量 | 说明 | 定义在函数外的变量 |
作用域 | 整个进程 | |
生存周期 | 整个进程 | |
存储位置 | 未初始化的在bss段,值一定是0 已初始化的在data段 | |
局部变量 | 说明 | 定义在函数内的变量 |
作用域 | 函数内 | |
生存周期 | 到函数结束 | |
存储位置 | 栈区 未初始化一定是随机值 | |
局部静态变量 | 说明 | 由static修饰的局部变量 |
作用域 | 函数内 | |
生存周期 | 整个进程 | |
存储位置 | 未初始化的在bss段,值一定是0 已初始化的在data段 |
-----------------------------------------------------------------------------------------
1)作用
缩短变量名的作用域(只在命名空间中有效,用来防止多文件多模块编程时出现命名冲突的问题)
2)语法
定义命名空间的方式
namespace myTestname
{
int a; // 整型变量
FILE *fp; // 文件流指针
void test(void); // 函数的声明
} // 注意:没有分号
3)打开命名空间
[1]使用域作用限定符号
命名空间的名字::变量名
例如:myTestname::a、std::cout
[2]展开命名空间(把限制打开)
using namespace 命名空间的名字
例如:using namespace std;、usingnamespace myTestname;
注意:在写项目时不适合用第二种方式,命名空间中的所有成员都在全局域中,这样会有冲突
4)输入输出
C语言是面向过程的语言,输入输出是函数提供的
C++是面向对象的语言,输入输出是对象
[1]标准输入istream----预定义的对象cin
cin >> n; // 读取一个数据,存储到n中
在C语言中,我们使用scanf给一个变量录入数值。
[2]标准输出ostream----预定义的对象cout
cout << "hello" << endl; // 输出一个字符串hello
endl(end of line)是C++标准库中的操控器,代表换行
在输入和输出时用到了“<<”和“>>”,其实就是位运算符,只不过运算符重载了。
cin和cout是由iostream头文件提供的对象,所以想要使用的话,需要包含这个头文件。
3、动态存储空间的开辟和释放
谁打开谁关闭 文件IO
谁申请谁释放 堆区的空间(malloc realloc calloc / free)
谁创建谁销毁 链式存储结构
谁加锁谁解锁 线程
C语言 | C++ | |
---|---|---|
内存开辟(堆区) | malloc、realloc、calloc | new |
内存释放(堆区) | free | delete |
在C++中对内存的开辟与释放需要用到两个关键字(new、delete)
new、delete不是C++某个类的对象,是运算符
new是内存分配运算符
delete是取消内存分配运算符
#include <iostream>
using namespace std;
int main(void)
{
int *p = NULL;
p = new int;//动态存储空间的开辟(等价于p = malloc(sizeof(int));)
if(p == NULL)//判断开辟动态存储空间是否失败
{
cerr << "Dynamic memory is Failed!" << endl;
return -1;
}
*p = 9527;
cout << "[1]p = " << p << " *p = " << *p << endl;
cin >> *p;
cout << "[2]p = " << p << " *p = " << *p << endl;
delete p;//释放动态存储空间(等价于free(p))
p = NULL;//为了防止野指针
return 0;
}
#include <iostream>
using namespace std;
int main(void)
{
char *s = NULL;
s = new char[32];//动态存储空间的开辟(等价于s = malloc(sizeof(char) * 32))
if(s == NULL)//判断动态存储空间开辟是否失败
{
cerr << "Failed!" << endl;
return -1;
}
cin >> s;//通过终端录入字符串给s
cout << "s = " << s << endl;//打印输出字符串
delete[] s;//释放动态开辟的存储空间(等价于free(s))
s = NULL;//
return 0;
}
4、函数 (参数从右向左依次入栈)
1)函数参数的默认值
在C++中,当我们定义了一个函数,可以为参数列表中的参数指定默认值,当调用该函数时,如果实际参数的位置空着不写,则使用这个默认值。这是通过在函数定义中使用赋值运算符来为参数赋值,调用函数如果没有传参,则默认使用该数值。若指定了值,则使用传递的值。
使用方法:
实例一:不带默认值
#include <iostream>
using namespace std;
int fun(int a, int b, int c);
int main()
{
fun(10, 20, 30); //若有默认值,且调用时未传参,则push 默认值
/*
push 30 //
push 20
push 10
call fun()
add esp,0ch
*/
return 0;
}
int fun(int a, int b, int c)
{
/*
push abp
mov ebp,esp
sub esp,Occh
……
*/
cout << a << '\n' << b << '\n' << c << endl;
return 0;
/*
mov eax,0
*/
}
实例二:函数声明与定义冲突
#include <iostream>
using namespace std;
int fun1(int a, int b, int c = 100);//定义时 c = 100,
int main()
{
fun1(10, 20, 30); //error 重定义默认参数: 参数 1
return 0;
}
int fun1(int a, int b, int c = 100)// c = 100 缺省参数重定义
{
cout << a << '\n' << b << '\n' << c << endl;
return 0;
}
实例三:声明点和定义点同时赋值
#include <iostream>
using namespace std;
int fun2(int a, int b, int c = 100);//定义时 b = 100,支持缺省一位参数
int main()
{
fun2(10, 20, 30);
fun2(10, 20);
//fun2(10);// error 函数不接受 1 个参数
/*
错误原因:程序顺序执行,在调用时只能看见该函数之前的声明。因此,在
函数定义点中的"b=100",处的默认参数无效。
*/
return 0;
}
int fun2(int a, int b = 100, int c)//编译通过,但 b 的默认赋值无用
{
cout << a << '\n' << b << '\n' << c << endl;
return 0;
}
实例四:多个声明中连续赋值
#include <iostream>
using namespace std;
int fun3(int a, int b, int c = 100);
int fun3(int a, int b = 100, int c);
int main()
{
fun3(10, 20, 30);
fun3(10, 20); // 10 20 100
fun3(10); // 10 100 100
return 0;
}
int fun3(int a, int b, int c)
{
cout << a << '\n' << b << '\n' << c << endl;
return 0;
}
注意事项:
-
从左到右原则:
如上所述,一旦某个参数被赋予默认值,其后的所有参数都必须有默认值。这是因为编译器需要能够区分哪些参数是明确提供的,哪些是默认提供的。 -
声明与定义:
默认参数值只能在函数声明中指定,而不能在函数定义中指定。如果同时有声明和定义,并且两者都试图为同一个参数指定默认值,这会导致编译错误。 -
头文件中的声明:
通常,函数的声明会放在头文件中,而定义则放在源文件(.cpp文件)中。因此,默认值也应该在头文件的函数声明中指定。
2)函数的重载
函数的重载就是函数名相同,参数列表不同(参数的个数、类型、顺序),返回值没有要求。
#include <iostream>
using namespace std;
void swap(int *a, int *b);
void swap(float *a, float *b);
int main(void)
{
int a1 = 0, b1 = 0;
float a2 = 0, b2 = 0;
a1 = 13, b1 = 7;
a2 = 13.7, b2 = 7.31;
#if 0
cout << "a1 = " << a1 << ", b1 = " << b1 << endl;
swap(&a1, &b1);
cout << "a1 = " << a1 << ", b1 = " << b1 << endl;
#else
cout << "a2 = " << a2 << ", b2 = " << b2 << endl;
swap(&a2, &b2);
cout << "a2 = " << a2 << ", b2 = " << b2 << endl;
#endif
return 0;
}
void swap(int *a, int *b)
{
int c = 0;
c = *a;
*a = *b;
*b = c;
}
void swap(float *a, float *b)
{
float c = 0;
c = *a;
*a = *b;
*b = c;
}
函数重载是一种多态性的表现形式,有助于提高代码的可读性和可维护性,当编译器遇到同名函数时,会根据实际的参数列表来决定调用哪个函数,从而提高更严格的函数类型检查,减少程序的错误。
C语言不支持函数的重载,在之前使用的open(2)函数并不是函数的重载,而是可变参(不定参)函数。
5、引用
引用相当于变量的别名,是某个变量的另一个名字,一旦把引用初始化为某个变量,就可以使用该引用名来指向这个变量。
#include <iostream>
using namespace std;
void swap(int *a, int *b); // 使用指针实现
void swap(float *a, float *b);
void swap(int &a, int &b); // 使用引用实现
int main(void)
{
int a1 = 0, b1 = 0;
float a2 = 0, b2 = 0;
a1 = 13, b1 = 7;
a2 = 13.7, b2 = 7.31;
#if 0
cout << "a1 = " << a1 << ", b1 = " << b1 << endl;
swap(&a1, &b1);
cout << "a1 = " << a1 << ", b1 = " << b1 << endl;
#else
cout << "a2 = " << a2 << ", b2 = " << b2 << endl;
swap(&a2, &b2);
cout << "a2 = " << a2 << ", b2 = " << b2 << endl;
#endif
cout << "a1 = " << a1 << ", b1 = " << b1 << endl;
swap(a1, b1);
cout << "a1 = " << a1 << ", b1 = " << b1 << endl;
return 0;
}
void swap(int *a, int *b)
{
int c = 0;
c = *a;
*a = *b;
*b = c;
}
void swap(float *a, float *b)
{
float c = 0;
c = *a;
*a = *b;
*b = c;
}
void swap(int &a, int &b)
{
int c = 0;
c = a;
a = b;
b = c;
}
-----------------------------------------------------------------------------------------
三种变量互换的实现
使用加减运算 | 使用异或运算 | 使用第三个变量 |
void swap (int &a, int &b) { a = a + b; b = a - b; a = a - b; } | void swap (int &a, int &b) { a = a ^ b; b = a ^ b; a = a ^ b; } | void swap (int &a, int &b) { int c = a; a = b; b = c; } |
-----------------------------------------------------------------------------------------
我们可以将变量名想象成是变量在内存位置中的标签,那么引用就是变量在内存中的第二个标签,因此我们可以通过引用来访问形参的内容。
引用类似于指针,但并不是指针,它们的不同:
【1】表达
指针是用来存储地址的
int i = 100; int *p = &i;
引用就是变量本身
int i = 100; int &p = i;
【2】占用空间
指针占用存储空间,在64bit的环境中占用8byte,在32bit的环境中占用4byte
引用不占用存储空间,就是一个别名
【3】空指针和空引用
程序中可以存在空指针(指向NULL的指针就是空指针)
int *p = NULL; // 定义一个空指针
程序中不可以存在空引用,引用必须连接到一块合法的内存中(一个存在的变量名)
【4】指向对象
指针(普通指针)可以在任何时候指向另一个对象,一旦引用被初始化为一个对象,就不能引用到另一个对象(有点像指针常量)
【5】初始化
指针可以在任何时候被初始化
引用必须在创建时被初始化
-----------------------------------------------------------------------------------------
指针与数据类型
问题:
a) 一个整型数(An integer)
b)一个指向整型数的指针( A pointer to an integer)
c)一个指向指针的的指针,它指向的指针是指向一个整型数( A pointer to a pointer to an intege)r
d)一个有10个整型数的数组( An array of 10 integers)
e) 一个有10个指针的数组,该指针是指向一个整型数的。(An array of 10 pointers to integers)
f) 一个指向有10个整型数数组的指针( A pointer to an array of 10 integers)
g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数(A pointer to a function that takes an integer as an argument and returns an integer)
h) 一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数( An array of ten pointers to functions that take an integer argument and return an integer )
i) 一个函数,该函数的参数是整数类型,该函数的返回值是指针。
答案:
a)int p;
b)int *p;
c)int **p;
d)int p[10];
e)int (*p[10]);
f)int (*p)[10];
g)int (*p)(int);
h)int (*p[10])(int);
i)int *p(int);
思考:
上面的问题和答案是可以反过来问的
再基于答案反推一遍,请描述上述数据类型的含义:
a)一个整数类型。
b)一个指针,指向整数类型。
c)一个指针,指向另一个指针,它指向的指针指向一个整数类型。
d)一个数组,它包含10个整数类型。
e)一个数组,它包含10个指针类型,指针指向整数类型。
f)一个指针,它指向一个数组,它包含10个整数类型。
g)一个指针,指向一个函数,该函数的参数是整数类型,该函数的返回值是整数类型。【函数指针】
h)一个数组,它包含10个指针类型,指针指向一个函数,该函数的参数是整数类型,该函数的返回值是整数类型。
i) 一个函数,该函数的参数是整数类型,该函数的返回值是指针,指向一个整数类型。【指针函数】
指针与关键字
将指针和const关键字相结合,就有了如下的题目:
a)char * const p;
b)char const *p;
c)const char *p;
d) char const* const p;
问题:请问上述三种数据类型的含义。
解答:
a)一个指针,指向char数据类型,p是常量。【指针不可更改】
b)一个指针,指向char数据类型,p指向的数据是常量。【所指的内容不可更改】
c)一个指针,指向char数据类型,p指向的数据是常量。(char放在前后都没关系)
d)一个指针,指向char数据类型,p是常量,p指向的数据也是常量。【指针和所指向的数据都不可更改】
思考:
试试总结一下static关键字与指针的关系。
指针与数组
将指针和数组相结合,就有了如下的题目:
char *p="abcdef";
char a[]="abcdef";
问题:请问p[i]与a[i]的区别?
解答:
首先,"abcdef"是一个字符串,在编译的时候,字符串被放在常量区。(挖坑,C:请描述一个可执行程序占用的内存分为哪几个区?每个分区各自的作用?)
*p是指针,指向常量区的字符串。
a[]是数组,在程序执行时,会从常量区拷贝"abcdef"到栈上,再继续后续的操作。
因此,这两个变量的指向的内存区域不同,打印的内存地址不同。
思考:
在上述题目的基础上,加上如下代码,编译是否会出错?为什么?
p[1]='h';
a[1]='h';
【提示:常量区数据无法修改】
指针、数组、关键字
上面的确定都会了?
将指针和关键字结合,再和数组结合,试试看做这题:
char a[]="abcdef";
char b[]="abcdef";
const char c[]="abcdef";
const char d[]="abcdef";
char *e="abcdef";
char *f="abcdef";
const char *g="abcdef";
const char *h="abcdef";
问题:
a,b地址是否相等?
c,d地址是否相等?
e,f地址是否相等?
g,h地址是否相等?
答案:
a!=b;
c!=d;
e==f;
g==h;
解答:
e,f,g,h都是指针,指向常量区的字符串,该字符串在内存中的地址是固定的。
a,b,c,d都是数组,在程序执行时,从常量区拷贝数据到栈中,每个数组的在栈中都会被分配地址。
指针函数与函数指针
C:什么是指针函数?什么是函数指针?他们之间有什么区别?
【快速记忆法:末尾是什么,他就是什么类型】
指针函数:一个函数,它的返回值是指针。
函数指针:一个指针,它指向一个函数。
【函数指针】定义形式:
类型 (*指针变量名)(参数列表);
例如:
int (*p)(int i,int j);
说明:
p是一个指针,它指向一个函数,该函数有2个整形参数,返回类型为int。
p首先和*结合,表明p是一个指针。然后再与()结合,
表明它指向的是一个函数。指向函数的指针也称为函数指针。
【指针函数】定义形式:
类型 *指针变量名 (参数列表):
例如:
int *p(int i, int j);
说明:
p是一个函数,该函数有2个整形参数,它的返回值是一个指针,指向一个整数类型。
因为()的优先级比*更高,所以可以写成:int *(p(int i,int j)),
表明它是一个函数,返回值是一个指向整型的指针。
-----------------------------------------------------------------------------------------
三、 面向对象
1、有关C语言的编程思想
面向过程(procedure oriented programming)简称POP的编程语言
C语言项目工程:
[1]划分模块
[2]每一个模块定义数据类型 + 函数接口
[3]实现函数功能
[4]通过调用函数最终实现项目功能
这是典型的面向过程的编程思想,分析出解决问题所需要的步骤,用函数将这些步骤一步一步实现,使用的时候,一个一个进行调用。
2、有关C++语言的编程思想
面向对象(object oriented programming)简称OOP的编程语言
C++项目工程:
[1]划分模块
[2]抽象类,类中有各种数据类型和函数
[3]通过实例化对象,驱动对象的函数
[4]最后实现项目工程
这是典型的面向对象的编程思想,把构成文件的各个事务分解成各个对象,实例化对象的目的不是为了完成一个步骤,而是为了描述一个事务在整个解决问题的步骤中的行为。
3、例子对比
1)五子棋游戏
面向过程 | 面向对象 |
---|---|
1、画棋盘 2、player1下棋 3、画棋盘 4、判断输赢 5、player2下棋 6、画棋盘 7、判断输赢 ------循环------ | 1、玩家类 实例化两个对象player1和player2, 下棋 2、棋盘类 实例化一个对象, 画棋盘 3、规则类 实例化一个对象, 判断输赢 |
2)洗衣服
面向过程 | 面向对象 |
---|---|
1、放衣服 2、放洗衣液 3、放水 4、开始洗衣 5、甩干 6、取衣服 | 1、人类 放衣服、放洗衣液、取衣服 2、洗衣机类 放水、开始洗衣、甩干 |
3)史书
面向过程比喻为编年体(主要以时间为主线,按照年月日的顺序来记述历史事件) --- 春秋
面向对象比喻为纪传体(主要以人物为中心,通过记述各种人物的活动来反映历史事件) --- 史记
-----------------------------------------------------------------------------------------
各专有名词的区别
匿名管道-----命名管道
进程间通信(IPC)
每个进程有各自不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到。所以进程之间交换数据,需要依靠内核(kernel),在内核开辟一块缓存区,进程1把数据从用户空间拷贝到缓存区,进程2再从缓存区把数据读走。内核提供的这种机制就是进程间通信。
匿名管道(pipe):
管道是IPC最基本的一种实现机制,在Linux下,“一切皆文件”,这里的管道就是一个文件。管道实现进程通信就是让两个进程都能访问该文件。
管道的特征:
- 只提供单向通信(一方读,一方写)
- 只能用于具有血缘关系的进程间通信(父子进程、兄弟进程)
- 管道是基于字节流来通信的
- 依赖于文件系统,生命周期随进程结束
- 机制本身带同步互斥效果
实现方式:
创建管道:
int pipe(int pipefd[2])
注释:调用pipe函数时,首先在内核中开辟一块缓冲区用于通信,有一个读端和写端,通过pipifd参数传出给用户进程两个文件描述符,pipefd[0]指向读端,pipefd[1]指向写端。在用户层面,打开管道就是打开了一个文件,通过read(2)读数据,write(2)写数据。
头文件:<unistd.h>
返回值:成功返回0,失败返回-1,设置errno
创建子进程:
pid_t fork(void);
注释: 调用fork函数时,程序会创建一个子进程,将父进程完全拷贝一份,包括当前程序运行到的位置,使程序进行并发运行,具体谁先谁后,要看处理器的调度策略。
头文件:<sys/types.h>、<unistd.h>
返回值:成功返回子进程的pid号,为0;失败返回-1,并设置errno;
实现步骤:
1】调用pipe函数,有父进程创建管道,得到两个文件描述符指向管道的两端
2】父进程调用fork创建子进程,通过拷贝,子进程也有两个文件描述符指向管道的两端
3】父进程关闭读端(0),只进行写操作;子进程关闭写端(1)只进行读操作,管道是通过唤醒队列实现的,数据从写端流入读端,从而实现进程间通信。
注意事项:
1】如果所有指向管道写端的文件描述符都关闭了,而仍然有进程从管道的读端读数据,那么文件内的所有内容被读完后再次read就会返回0,就像读到文件结尾。
2】如果有指向管道写端的文件描述符没有关闭(写端的引用计数大于0),而持有管道写端的进程没有向管道内写入数据,如果这时有进程从管道读端读数据,那么读完管道内剩余的数据后会阻塞等待,直到有数据可读才读取数据并返回。
3】如果所有指向管道读端的文件描述符都关闭,此时有进程通过写端文件描述符向管道内写数据时,进程会收到SIGPIPE信号,并异常终止。
4】如果有指向管道读端的文件描述符没有关闭(读端的引用计数大于0),而持有管道读端的进程没有从管道内读数据,如果这时有进程向管道写端写数据,那么管道被写满后会阻塞等待,直到管道内有空位置后才写入数据并返回。
命名管道(FIFO)
它提供了一个路径名与之关联,以FIFO文件的形式存储在文件系统中,可以实现任意两个进程之间的通信,而匿名管道对于文件系统是不可见的,它仅限于在父子进程之间的通信。
FIFO是一个设备文件,在文件系统中以文件名的形式存在,因此即使进程与创建FIFO的进程不存在血缘关系也依然可以通信,前提是可以访问该路径。
FIFO(first input first output)总是总是遵循先进先出的原则,第一个进来的数据会被第一个读走。
实现方式(两种):
1、在shell下使用命令mknod或mkfifo创建命名管道,进程之间使用read\write进行通信
2、系统函数创建
报式套接字-----流式套接字
文件io-----系统io
II2C-----SPI
uart-----usart
重入------重载
-----------------------------------------------------------------------------------------
任务
1、了解RTOS(FreeRTOS / RTThread)
FreeRTOS:
RTThread:
2、了解STM32CubeMx工具
3、排序算法
1)冒泡排序(Bubble Sort)
它依次遍历要排序的元素序列,比较相邻两个元素,如果顺序不满足要求,就把它们的位置互换,直到遍历完序列中所有元素均满足要求,则说明该元素序列 已经排序完成,程序退出。
因为在进行排序时,越小的元素(降序排列)会经由交换,慢慢的“浮”到数列的顶端,就像碳酸饮料的二氧化碳气泡慢慢浮起,故名“冒泡排序”。
#include <iostream>
#define LEN 15
using namespace std;
void swap(int &a, int &b);
void bubble_sort(int arr[], int len);
void bubble_sort_pro(int arr[], int len);
int main(void)
{
return 0;
}
void swap(int &a, int &b)
{
int tmp = 0;
tmp = a;
a = b;
b = tmp;
}
void bubble_sort(int arr[], int len)
{
int i = 0, j = 0;
for (; i < len - 1; i ++) {
for (; j < len - i - 1; j ++) {
if (arr[j] < arr[j + 1]) // 降序排列
swap(arr[j], arr[j + 1]);
}
}
}
// 冒泡优化,添加标志位,避免不必要的比较
void bubble_sort_pro(int arr[], int len)
{
int i = 0, j = 0;
bool flag = true;
for (; i < len - 1; i ++) {
flag = true;
for (; j < len - i - 1; j ++) {
if (arr[j] < arr[j + 1]) { // 降序排列
swap(arr[j], arr[j + 1]);
flag = false;
}
}
if (flag)
break;
}
}
2)选择排序(Selection Sort)
它在每一趟从待排序列的元素中选出最小(或最大)的一个元素,放在待排序列的最前(或最后),直到全部待排序列的数据元素排列完毕。
#include <iostream>
#define LEN 15
using namespace std;
void swap(int &a, int &b);
void selection_sort(int arr[], int n);
int main(void)
{
return 0;
}
void swap(int &a, int &b)
{
int tmp = 0;
tmp = a;
a = b;
b = tmp;
}
void selection_sort(int arr[], int n)
{
int min = 0;
int i, j;
for (i = 0; i < n - 1; i ++) {
min = i;
for(j = i; j < n; j ++) {
if (arr[min] > arr[j])
min = j;
}
if (min != i)
swap(arr[min], arr[j]);
}
}
3)插入排序(Insertion Sort)
它通过构建一个有序序列实现排序,对于未排序的数据,它将这个数据与前面的数据进行比较,找到相应的位置进行插入,直到最后一个数据排序完成。
#include <iostream>
#define LEN 15
using namespace std;
void insertion_sort(int arr[], int n);
int main(void)
{
return 0;
}
void insertion_sort(int arr[], int n)
{
int i = 0, j = 0;
int sentry = 0; // 哨兵(存储需要改变位置的元素值)
for (i = 1; i < n; i ++) {
if (arr[i] < arr[i - 1]) {
sentry = arr[i];
j = i - 1;
// 升序排列
while (sentry < arr[j] && j >= 0) {
arr[j + 1] = arr[j];
j --;
}
arr[j + 1] = sentry;
}
}
}
4)希尔排序(Shell Sort)
它是简单插入排序的改进版,也称为缩小增量排序,它先选定一个跨度(增量),将待排序列按照跨度(增量)分割为若干子序列,分别进行直接插入排序,跨度(增量)每次缩小,直到缩小为1,就变为对整个序列进行直接插入排序,这时需要进行替换的元素变得很少,大部分只是比较操作,一定程度上提高了排序效率。
#include <iostream>
#define LEN 15
using namespace std;
void shell_sort(int arr[], int n);
int main(void)
{
return 0;
}
void shell_sort(int arr[], int n)
{
int i = 0, j = 0, k = 0;
int sentry = 0; // 哨兵(存储需要改变位置的元素值)
int gap = 0; // 跨度(增量)
for (gap = n / 2; gap > 0; gap /= 2) {
for (i = gap; i < n; i ++) {
sentry = arr[i];
j = i - gap;
// 升序排列
while (sentry < arr[j] && j >= 0) {
arr[j + gap] = arr[j];
j -= gap;
}
arr[j + gap] = sentry;
}
}
}
5)归并排序(Merge Sort)
它采用分治法(Divide and Conquer)的思想,将待排序列先分为长度为n/2的子序列,分别进行归并排序,最后将两个子序列合并成一个最终的有序序列。
#include <iostream>
#define LEN 15
using namespace std;
void merge(int arr[], int n);
void merge_sort(int arr[], int n);
int main(void)
{
return 0;
}
void merge(int arr[], int n)
{
int tmp[n] = {};
int pos = 0; // 存储元素的位置
int mid = n / 2;
int first = 0, second = mid; // 有序序列的起始位置
int i = 0;
while (first < mid && second < n) {
if (arr[first] < arr[second])
tmp[pos ++] = arr[first ++];
else
tmp[pos ++] = arr[second ++];
}
while (first < mid)
tmp[pos ++] = arr[first ++];
while (second < n)
tmp[pos ++] = arr[second ++];
for (i = 0; i < n; i ++) // 将排好序的子序列合并为一个有序序列
arr[i] = tmp[i];
}
void merge_sort(int arr[], int n)
{
if (n > 1) {
merge_sort(arr, n / 2);
merge_sort(arr + n / 2, n - n / 2);
merge(arr, n);
} else {
return ;
}
}
6)快速排序(Quick Sort)
每次选择一个基准值,将小于基准值的放前边,将大于基准值的放后边,再对两边重新选择基准值,递归实现。
#include <iostream>
#define LEN 15
using namespace std;
void partition(int arr[], int left, int right);
void quick_sort(int arr[], int start, int end);
int main(void)
{
return 0;
}
void partition(int arr[], int left, int right)
{
int key = arr[left];
int pos = left;
while (left < right) {
while (left < right && arr[right] >= key)
right --;
arr[pos] = arr[right];
pos = right;
while (left < right && arr[left] <= key)
left ++;
arr[pos] = arr[left];
pos = left;
}
arr[pos] = key;
return pos;
}
void quick_sort(int arr[], int start, int end)
{
if (start >= end) {
return ;
}
int pos = partition(arr, start, end);
quick_sort(arr, start, pos - 1);
quick_sort(arr, pos + 1, end);
}
各排序算法对比