《算法笔记》的笔记
第二章: C/C++快速入门
文章目录
前言
在学习之前有一个小插曲,用vi命令打开.c文件的时候总是提示存在交换文件,这让我没有办法修改我的目标文件,经过查询相关资料,我发现只要将对应的交换文件删除就可以了,产生原因可能是因为在退出编辑模式之前退出文件了。应该是shift+double z,而不是crtl
具体的操作方法:使用ls - al 命令查看文件夹目录,找到交换文件(文件后缀是.swp)然后就用 rm - 文件名 删掉相应的交换文件就可以了
PS:我发现当交换文件出现重名的时候,文件的拓展名会到发生变化(.swn 、.swo等)
PPS:很久没有用我的Ubuntu了,很多的命令都忘记了,所以一些常用的命令需要在这里记下来,以便之后学习的时候复习。
vi/vim、cd、ls、mkdir
虽然之前学过C/C++但是担心自己有什么遗漏所以还是看了一遍,在这里主要记录的就是自己原先不是很熟悉的内容。
2.1 基本数据类型
- cin和cout消耗的时间会比scanf和printf多得多。而且在同一个程序当中不应该同时使用cout和printf(会出现问题)
- C++标准库中,stdio.h的推荐写法:cstdio(就是去掉后缀,然后在最前面加上c,eg:<math.h> —> <cmath>)
- 基本数据类型:
i)整型:int —> 32bit (10^9以内使用)|| long long ----> 64 bit(如果long long型赋大于2^31-1的初值的时候,需要在初值后面加上LL,否则会编译错误)
ii)浮点型:(Mantissa:小数部分)
由上图可以知道单精度float有效精度只有6~7位(具体原因之后补充),双精度浮点型double的有效精度为15~16位。书中的建议:(应该是单精度浮点型的精度在大多时候都不够用吧)
iii)字符型:字符在C语言中使用使用ASCII码统一编码的。(需要记住一点小写字母比大写字母的ASCII码值大32,0的ASCII码为48) (还要记住几个常用的转义字符:\n代表换行,\0代表空字符,其ASCII码为0,\t代表Tab键) 这一部分总结来说就是%c会将整型数字转化成相应ASCII码所对应的值。 对于字符串型可以转化成字符数组,但是不能赋值给字符变量对于浮点型来说,只需记住一点,不要使用float,碰到浮点型的数据都应该使用double来存贮。
3.强制类型转换
格式:(新类型名)变量名
如果在计算过程中需要类型转换,那么就不能等它算完再在赋值的时候转换
4.符号常量和const常量
这是定义常量的两种方式(更加推荐后者)
格式:
#define 标识符 常量//注意这里最后是没有分号的
const 数据类型 变量 = 常量;
宏定义(Macro definition)的陷阱:
#include <stdio.h>
#define CAL(x) (x * 2 + 1)
int main(void){
int a = 1;
printf("%d\n",CAL (a + 1));
return 0;
}
//输出:4
//因为宏定义只会将数据原封不动地带进去,实际上的运算过程为:a+1*2+1.
//所以在用宏定义的时候应该将变量全部用括号括起来
5.运算符
i)算数运算符没有幂次运算符,^代表的是位异或(位运算符可以用来定义无穷大的数)
//实际上就是整型的上限
const int INF = (1 << 30) -1;
const int INF = 0x3fffffff;
位运算符的使用技巧:
//1.判断两个数字是否异号,利用补码编码的符号位进行异或运算(同0异1)
int i = -1, j = 1;
(i ^ j < 0) = true;
//2.将编码最后一个1转换成0,可用于求解统计二进制编码中1的个数和判断一个数是不是2的指数
//这个数减去一之后,会向从右开始的第一个数借一位,这使得这一位变成0,然后与运算,
//将不相同的位全部变成0,这样就去掉了最右边的哪一个1
n & (n-1);//可以用于统计1的个数,或者进行相应的运算
//统计一个数的二进制数中1的个数(汉明权重)
int hammingWeight(uint32_t n){
int res = 0;
while(n != 0){
n = n & (n - 1);
res++;
}
}
//判断一个数是否为2的指数:注意2的指数有一个特点,在二进制编码中只能有一个1
bool isPowerOfTwo(int n){
if(n <= 0)return false;
return (n & (n - 1)) == 0;
}
ii)条件运算符 格式:A ? B : C;
//用条件运算符+宏定义判断大小的结构
#define MAX(a, b) ((a) > (b) ? (a) : (b))
2.2 顺序结构
2.2.1 赋值语句
而如果要给多个变量赋同一个值,可以使用连续等号的方法:
int n, m;
n = m = 5;
2.2.2 输入/输出语句(scanf/printf)
1.scanf函数
注意到字符串类型不需要使用取址运算符,因为字符串类型相当于一个数组,数组名称代表第一个元素的地址。
另外,除了%c 外, scanf 对其他格式符(如%d)的输入是以空白符(即空格、 Tab )为结束判断标志的
int main (void){
int a, a1;
scanf("%d%d", &a, &a1);
printf("%d %d", a, a1);
}
//input:1 2 output:1 2
但是对于%c
int main(void){
int a; char c, str[10];
scanf("%d%c%s", &a, &c,str);
printf("a=%d, c=%c,str=%s", a, c, str);
return 0;
//Input:1 2 3
//Output:a = 1, c=, str = 2
//可见空格也被当成一个字符了
}
scanf()的返回值:
输入数字,匹配%d,配置b的值成功,scanf返回成功配置数量1;
输入字母,不匹配%d,配置b的值失败,scanf返回成功配置数量0;
输入Ctrl+Z,scanf返回-1;
2.printf函数
这里注意一点:在scanf中double的格式符为%lf,而在printf中格式符为%f
接下来介绍三种使用的输出格式:(m为一个具体的数字)
- %md 可以使不足 位的 int 型变量以m位进行右对齐输出,其中高位用空格补齐。如果变量本身超过m位,则保持原样。
- %0md 和上面的类似,只不过是用0而不是空格
- %.mf 让浮点数保留m位小数输出,采用”四舍六入五成双“[4]的规则
2.2.3 getchar & putchar
获取和输出一个字符,注意getchar()是可以识别换行的。
int main(void){
char c1, c2, c3;
c1 = getchar();
getchar();
c2 = getchar();
c3 = getchar();
putchar(c1);
putchar(c2);
putchar(c3);
}
//Input:ab(<Enter>)c
/*
Output:
a
c
*/
2.2.5 typedef
可以给复杂的数据类型起一个别名
typedef long long LL;//之后就可以用LL代替long long了
2.2.6 常用的math函数
需要#include<math.h>
//只能是括号中的类型(?)
1. fabs(double x)————(float absolute value)给double型变量取绝对值
2. floor(double x) & ceil(double x) ————(地板和天花板)向下和向上取整
3. pow(double r, double p)————(幂函数:power function)返回 r^p
4. sqrt(double x) ————(Arithmetic square root)返回算数平方根
5. log(double x) ————(Natural logarithm) 返回以自然对数为底数的对数值
6. sin(double x) 、cos(double x)、tan(double x) ————可以用来精确定义pi的值:acos(-1.0)
7. asin(double x) 、acos(double x)、atan(double x) ————反三角函数
8. round(double x) ————(round:用整数表示的, 取整数的, 整数的)四舍五入取整
C语言中没有对任意底数求对数的函数,因此必须使用换底公式来将不是以自然对数为底的对数转换为以e为底的对数,即
l
o
g
a
b
=
l
o
g
e
b
/
l
o
g
e
a
log_ab = log_eb/log_ea
logab=logeb/logea
注意:pow()可能导致错误的情况:
- 如果底数 x 为负数并且指数 y 不是整数,将会导致 domain error 错误。
- 如果底数 x 和指数 y 都是0,可能会导致 domain error 错误,也可能没有;这跟库的实现有关。
- 如果底数 x 是 0,指数 y是负数,可能会导致 domain error 或 pole error 错误,也可能没有;这跟库的实现有关。
- 如果返回值ret 太大或者太小,将会导致 range error 错误。
原文链接:https://blog.csdn.net/yuanbo_shaw/article/details/79511132
2.3 选择结构
因为这部分的内容比较熟悉,就跳过了。
switch(表达式){
case 表达式1://相当于switch中的语句的结构,如果匹配,则执行之后的语句
...
break;
case 表达式2:
...
break;
.........
default:
....
//注意每个case语句之后是没有大括号的!
//break是用来结束当前switch语句的
对于for语句,在C语言中是不允许定义变量的,但是在C++中可以(将代码文件保存为.cpp的格式即可)
2.5 数组
2.5.1 一维数组
- 数组的大小必须是整型常量,不能是变量。但是可以自己申请空间以定义数组
//C
int* a = (int*)malloc(sizeof(int)*n);
free(a);
//C++
int* a = new int[n];
delete[] a;
//释放内存~~~
- 当数组没有被初始化值的时候,一般默认是赋值为0,但是有时候有可能被赋值为很大的随机数
- 给数组赋值的方法:
int a[10] = {0};//这种方法只能用于将数组初始化为0
int a[10] = {-1};//-1 0 0 0 0 0 0....(测试环境VS2019)
fill (a, a + 10, -1);//初始化为-1,需要函数头algorithm和using namespace std;
int a[10] = {};//初始化为0
2.5.2 冒泡排序
本质上就是通过不断地交换减少数组中的逆序数对。在一遍遍历过程中,不断将较大的元素与相邻的元素进行交换。而最大的元素因为总是会比之后的元素更大,所以会被移到最后。
void BubbleSort(int a[],int n){
int sorted = false;
while(!sorted){
sorted = true;//优化
for(int i = 1; i<=n;++i){
if(a[i-1] > a[i]){
int temp = a[i-1];
a[i-1] = a[i];
a[i] = temp;
sorted = false;
}
}
}
}
2.5.3 二维数组
//二维数组想要作为参数传入函数中,需要确定二维数组的大小(至少是第二维需要确定)
//二维数组的赋值方式
int a[5][6] = {{3,1,2},{8,4},{},{1,2,3,4,5}};//剩余部分将会被赋值为0
注意:当数组大小较大时(
1
0
6
10^6
106数量级),需要将其定义在主函数外面,否则会使程序异常退出。
因为在函数内部申请的局部变量来自于系统栈,允许的空间较小;函数外部申请的全局变量来自静态存储区,允许申请的空间大。[6]
2.5.4 memset函数
menset函数可以给数组中的每一个函数赋值,但是menset使用的是按字节赋值的,这就是说一个整型变量的四个字节都会被赋相同的值。
//memset(数组名,赋值,sizeof(数组名));
int main(void){
int A[5] = { 1,2,3,4,5 };
memset(a, 1, sizeof(A));
for (int i = 0; i < 5; ++i) {
printf("%d\t", a[i]);
}
}
//输出:16843009 16843009 16843009 16843009 16843009[5]
//赋值为0或者-1(0的二进制补码为全0,-1的二进制补码为全1)
//还可以用fill函数对数组进行赋值,但是执行速度较差。
这里每个字节的意思是每个字节都设为1,注意不要与每个位赋值弄混淆。因为int是8字节所以,调用menset之后,数组中存储的数字为:0000 0001 0000 0001 0000 0001 0000 0001.(1字节为8位)
2.5.5 字符数组
输入:(scanf)
%c:输入单个字符(能识别空格与换行并将其输入)
%s:输入字符串并保存在字符数组中(以空格或者换行作为结束标志)
(gets)输入一行字符串,以\n作为结束标识。
(getchar)获取输入额一个字符
char str2 [5][10];//定义
...
for(int i = 0; i < 3; ++i){
gets(str2 [i]);//运用这种方法将将输入的第一行赋值给数组的一整列
}
//输出格式类似,运用的函数为puts(与gets类似)
注意:字符串是以‘\0’(ASCII 为 0)作为结尾的,所以存储的长度应该比实际存储长度大一。还有当用gets或者scanf函数的输入时,会自动在字符串尾部加上‘\0’,但是当用getchar函数(getchar每次只获取一个字符,所以’\0’作为单独字符需要调用额外的getchar函数)时,一定要在每个字符串后面加上‘否则会出现乱码
char str[15];
for (int i = 0; i < 3; ++i) {
str[i] = getchar();
}
puts(str);
return 0;
//Input:T^T
//Output:T^T烫烫烫烫烫烫烫烫烫烫坍G醶?烫烫哎^v€麧
//应该是程序不知道什么地方是结束
2.5.6 string.h头文件
#include<string.h>
...
char str1[10], str2[10];//代表字符数组
strlen(str1);//得到第一个'\0'之前的字符个数
strcmp(str1,str2);//按照字典序比较(<:返回负整数;=:返回0;>:返回正整数)
strcpy(tr1,str2);//str2复制给str1,包含'\0'
strcat(str1,str2);//(concatenate:把事物连接起来)str2接到str1后面
2.5.7 sscanf & sprintf
(都在stdio.h中)(补充)
//这个函数的主要用途就是将字符数组的内容转化成整型,放到整型变量中
int n;
char str[100] = "123";
sscanf(str, "%d", &n);//相当于将str作为输入对象(类似于scanf的屏幕输入)
printf("%d\n", n);
//就像printf一样,只不过是将其打印到了一个字符串数组中去了
double db = 3.1415;
int n1 = 12;
char str1[100], str2[100] = "good";
sprintf(str1, "%d:%.2f,%s", n1, db, str2);//将这些元素输出给str1
printf("str1 = %s\n", str1);
//输出:
// 123
// str1 = 12:3.14,good
2.6 函数
函数这部分也比较熟悉所以大部分省略了
1.数组可以作为参数,但是不允许作为返回类型出现
2.7 指针
指针(英语:Pointer),是编程语言中的一类数据类型及其对象或变量,用来表示或存储一个存储器地址,这个地址的值直接指向(points to)存在该地址的对象的值。
相关的操作有:
& ——取此变量的地址
* ———取该地址对应的值
指针变量的运算:加(减)代表的是指针指向前一个(后一个)存储块。看下面这个例子
int* x, int y
...
x = &y + 3 //结果是x存储的地址是y的地址+12
//因为一个int类型占4个字节,所以这样总的来说就是向前移动了12个字节
在函数中,想要修改传入的值,就需要传入该变量的地址(传址访问),包括要修改指针变量也是需要将指针变量所对应的地址传进去。
注意:常量是不能被使用取址运算符。因为引用的对象必须是可以修改的左值,也就是说放常量被引用的时候,它并不能被修改,所以索性就不能被引用了。
在数组中,一下两种检索方式是等价的
& big_array[ 0 ] + i * sizeof( big_array[ 0 ]);//对地址直接修改
&big_array[ i ] = big_array + i;//通过指针运算
(以下来自于《C程序语言设计2e》)
Pointers and Arrays
注意事项:
1.(*ip)++这里的括号是必要的,因为单元运算符像*、++结合,是从右向左运算的。(一个准则,当你遇到不熟悉的运算符的时候,将所有可能出错的地方全部加括号)
2.在传参的时候,想要修改传入参数,需要传入参数的地址,例如swap(&a,&b);
3.int a[num],这是在申请大小为num的数组,索引范围为0 ~ num-1
2.8 结构体(struct)的使用
2.8.1 结构体的定义方式以及访问方式
注意在结构体内不能用定义的结构体定义变量,但是可以定义指针类型。
//结构体的定义方式
struct structname{
int a;//成员变量
structname b;//不能定义自己本身(会引起循环定义的问题)
structname* c;//可以定义自身类型的指针变量
}A;//结构体变量,相当于 structname A;
结构体内元素的访问方式:
点运算符:”.“非指针变量的访问方式;
指针运算符:”->“指针变量的访问方式。
2.8.3 结构体初始化
构造函数是用来初始化结构体的一种函数。
//默认情况下构造函数是不显示的
//但是想要自己重新定义构造函数,就不能不经初始化就定义结构体变量
//想要同时实现二者,需要像以下这样定义构造函数
//只要参数个数和类型不完全相同,就可以定义多个构造函数,以适应不同环境下的初始化
struct studentInfo{
int id;char gender;
studentInfo(){};//默认的构造函数
studentInfo(char _gender){//这里的变量名不能和外部的一样
gender = _gender;
}
studentInfo(int _id, char _gender){
id = _id;
gender = _gender;
}
};
//初始化方式
studentInfo A;
A = studentInfo(2021,"girl");//调用构造函数初始化
//C++的另一种构造函数的方式
studentInfo(int _id, char _gender):id(_id),gender(_gender){};
2.9 补充内容
2.9.1 cin 与 cout
这两个输入输出函数虽然简洁(不用定义类型之类的)但是面对大量数据的时候,效率较低。
//">>"可以看成数据流的方向
//多变量输入
cin >> n >> db >> c >> str;
//多变量输出
cout << n << "\n" << db << endl;
2.9.2 浮点数比较
由于在计算机中采用有限位的二进制编码,因此浮点数在计算机中的存储并不总是精确的[8]。
在经过大量运算之后,浮点数的精度可能会发生变化,这就给比较操作(>、<、=)
产生了干扰。所以,我们定义一个极小数eps(通常取
1
0
−
8
10^{-8}
10−8),只要比较数落在特定的区间内(
±
e
s
p
\pm esp
±esp)即可。
//用宏定义等于
const double esp = 1e-8;
#define Equ(a,b) ((fabs((a)-(b)) < (esp)))
其他的比较运算符的所在区间如下图:
还有几点需要注意:
- 经过大量的运算之后,一个变量存储的0可能是一个很小的负数,这时候使用sqrt函数就会报错,这时候就应该运用esp使变量保证在定义域内(*)
- 某些编译环境中,可能会输出-0.00.这是能将此输出放到字符串中,与“-0.00”比较,如果一致,那就加上esp修正这个值。
2.10 黑盒测试
补充内容
动态数组
在定义数组之前,若数组大小未知,则需要对数组的空间进行申请
//一维数组
//动态分配,数组长度为m
int *array = new int [m];
//释放内存
delete[] array;
//定义结构体动态数组并初始化
#include<iostream>
#include<algorithm>
struct Mystruct{
int a, b;
}
int main(){
Mystruct* structs = new Mystruct[100];
Mystruct initializer = {20, 30};
fill(structs, structs + 100,initializer);
}
C语言的共生体
参考:https://www.runoob.com/cprogramming/c-unions.html
共生体是一种能在相同的内存位置存储不同的数据类型的一种特殊的数据类型。在共生体中能定义很多的不同的成员变量,但是任何时候只能有一个成员带有值。这样做可以节省空间,可以应用于变量不会同时使用的情况。
通信中的数据包会用到共用体:因为不知道对方会发一个什么包过来,用共用体的话就很简单了,定义几种格式的包,收到包之后就可以直接根据包的格式取出数据。
定义方式
union [union tag]
{
member def;
menber def;
....
}[union variable(s)];
//实例:成员变量可以是自定义的结构类型
union Data
{
int i;
float f;
char str[20];
}data;
共用体占用的内存应足够存储最大数据类型成员。
访问共生体成员
想要访问共生体成员需要使用点运算符。但是因为共生体所有的成员共用一个空间,所以在同一时间只能有一个成员变量能够拥有完整的赋值。
//数据损坏
union Data data;
//这样只有data.str能够拥有完成的数据
data.i = 10;//在后续的赋值过程中对应的内存被占用
data.f = 220.5;
strcopy(data.str, "Union");
实例
判断机器是大端机还是小端机
union
{
char str;
int data;
};
data=0x01020304;
if(str==0x01)
{
cout<< "此机器是大端!"<<endl;
}
else if(str==0x04){
cout<<"此机器是小端!"<<endl;
}
else{
cout <<" 暂无法判断此机器类型!"<<endl;
}
C 位域
C语言提供了一种更好的利用内存空间的方式。这种方式可以告诉编译器你只用这些字节
struct
{
unsigned int withValidated : 1;//只用一位来存储该变量
unsigned int heightValidated : 1;//后面的数字表示数据占用的空间的大小
}status;
struct
{
unsigned int age : 3;
}Age;
Age,age = 7;//能够表示的最大值
Age.age = 8;//输出的时候为0,溢出
枚举类型
定义格式
enum name {eleName1, eleName2,....};
enum DAY
{
MON = 1,TUE,WED,THU,FRI,SAT,SUN
}day;//1,2,3,4,5,6,7
//enum DAY day;
day = WED;
printf("%d",day);//3
enum seasons{
spring, summer = 3, autumn, winter;
};//0,3,4,5
友元函数
在面向对象编程中,友元函数(friend function)是一个指定类(class)的“朋友”,该函数被允许访问该类中private、protected、public的资料成员。普通的函数并不能访问这些资料,然而宣告一个函数成为一个类的友元函数则被允许访问这些资料。
友元函数的宣告可以放在类声明的任何地方,不受访问限定关键字private、protected、public的限制。一个相似的概念是友谊类。
友谊关键字应该谨慎使用。如果一个拥有private或者protected成员的类,宣告过多的友元函数,可能会降低封装性的价值,也可能对整个设计框架产生影响。
友元函数在类内部定义,可以在类外部定义。定义时在前面加上一个friend
关键字
class Box
{
double width;
public:
double length;
friend void printWidth(Box box);
}
void printWidth(Box box)
{
cout << box.width << endl;//访问私密成员
}
重载
C++ 允许在同一作用域中的某个函数和运算符指定多个定义,分别称为函数重载和运算符重载。
重载函数应该有不同的输入参数,这样能够编译器会根据不同的输入参数确定函数的定义,这个过程叫做重载决策
函数重载
class printData
{
public:
void print(int i) {
cout << "整数为: " << i << endl;
}
void print(double f) {
cout << "浮点数为: " << f << endl;
}
void print(char c[]) {
cout << "字符串为: " << c << endl;
}
};
运算符重载
重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。
//将两个Box类相加
Box operator+(const Box&, const Box&);
Box box1, box2, box3;
....
box3 = box1 + box2;
文件(C++PrimerPlus)
文件本身就是一连串的存储在设备当中的字节。
文件输入和输出
文件输出流的类包含在文件头fstream(fstream.h)中。下面是写入文件的步骤:
- 创建一个ofstream对象
- 将这个对象链接到特定的文件
- 用输入输出流控制命令,对文件中的字节进行操作
打开一个文件可以用open()方法
//创建文件对象
ofstream fout;
//将文件对象与特定文件相互链接
fout.open("jar.txt");
//或者在定义对象的时候初始化链接
ofstream fout("jar.txt");
文件对象在输入或者输出的时候,会对每一个文件对象设立一个缓冲区,这个缓冲区存储输入的字节,当缓冲区满了以后,再将其中的内容转换到文件中。这种以缓冲区为单位一块块地传输字节,极大地提升了文件处理的速度。
读取文件中的内容
char ch;
fin >> ch;
char buffer[80];
fin.getline(buffer,80);//getline在string函数头中
string line;
getline(fin, line);
可以用close方法,断开文件对象与文件的链接,这样能够保证文件缓冲区就会被刷新,这样保证了文件会被实时更新。
下面是对文件的输入操作的实例。
void fileio17_16() {
string filename;
cout << "Enter name for new file: ";
cin >> filename;
ofstream fout(filename.c_str());//将C++的string类型转换成C语言的char数组,返回一个数组指针
fout << "For your eyes only!\n";
cout << "Enter your secret number:";
float secret;
cin >> secret;
fout << "Your secret number is " << secret << endl;
fout.close();
ifstream fin(filename.c_str());
cout << "Here are the contents of " << filename << ":\n";
char ch;
while (fin.get(ch))
cout << ch;
cout << "Done\n";
fin.close();
//return 0;
}
总结(主要记录遇到的问题)
1.想直接赋值字符串而不是字符(Solved)
字符串常量可以作为初值赋给字符型数组,并用%s输出
char strl[25] = "cser"; printf("%s", strl); //对于字符串型可以转化成字符数组,但是不能赋值给字符变量
2.用VS2019编译sanf无法通过,出现C4996错误。(Solved)
在 Visual Studio 中关闭项目的警告:打开项目的 “属性页” 对话框。 选择 “配置属性” " > c/c + + > 高级" 属性页。编辑 “禁用特定警告” 属性以添加 4996 。 选择 “确定” 以应用所做的更改。
3.浮点型的有效精度(?)
4.“四舍六入五成双”规则?
5.menset按字节赋值?
6.系统栈&静态存储区?
7.正则表达式?
8.浮点数不总是精确的详细原因